|
| 1 | +// deck.gl |
| 2 | +// SPDX-License-Identifier: MIT |
| 3 | +// Copyright (c) vis.gl contributors |
| 4 | + |
| 5 | +import {CompositeLayerProps, DefaultProps} from '@deck.gl/core'; |
| 6 | +import {_Tile2DHeader, TripsLayer, TripsLayerProps} from '@deck.gl/geo-layers'; |
| 7 | +import {GeoJsonLayer} from '@deck.gl/layers'; |
| 8 | + |
| 9 | +import type {TilejsonResult} from '@carto/api-client'; |
| 10 | +import VectorTileLayer, {VectorTileLayerProps} from './vector-tile-layer'; |
| 11 | +import {transformTrajectoryData, type TileBounds, normalizeTimestamp} from './trajectory-utils'; |
| 12 | +import {autocomputeSpeed} from './trajectory-speed-utils'; |
| 13 | +import {createBinaryProxy, createEmptyBinary} from '../utils'; |
| 14 | + |
| 15 | +const defaultProps: DefaultProps<TrajectoryTileLayerProps> = { |
| 16 | + ...TripsLayer.defaultProps, |
| 17 | + data: VectorTileLayer.defaultProps.data, |
| 18 | + autocomputeSpeed: false, |
| 19 | + renderMode: 'trips', |
| 20 | + currentTime: 0, |
| 21 | + trailLength: 1000 |
| 22 | +}; |
| 23 | + |
| 24 | +/** All properties supported by TrajectoryTileLayer. */ |
| 25 | +export type TrajectoryTileLayerProps<FeaturePropertiesT = unknown> = _TrajectoryTileLayerProps & |
| 26 | + Omit<TripsLayerProps<FeaturePropertiesT>, 'data'> & |
| 27 | + Pick< |
| 28 | + VectorTileLayerProps<FeaturePropertiesT>, |
| 29 | + 'getFillColor' | 'getLineColor' | 'uniqueIdProperty' |
| 30 | + >; |
| 31 | + |
| 32 | +/** Properties added by TrajectoryTileLayer. */ |
| 33 | +type _TrajectoryTileLayerProps = { |
| 34 | + data: null | TilejsonResult | Promise<TilejsonResult>; |
| 35 | + |
| 36 | + /** |
| 37 | + * Set to true to automatically compute speed for each vertex and store in properties.speed |
| 38 | + */ |
| 39 | + autocomputeSpeed?: boolean; |
| 40 | + |
| 41 | + /** |
| 42 | + * Rendering mode for trajectories. |
| 43 | + * - 'paths': Static path rendering |
| 44 | + * - 'trips': Animated trip rendering with time controls |
| 45 | + */ |
| 46 | + renderMode?: 'paths' | 'trips'; |
| 47 | +}; |
| 48 | + |
| 49 | +/** |
| 50 | + * Helper function to wrap `getFillColor` accessor into a `getLineColor` accessor |
| 51 | + * which will invoke `getFillColor` for each vertex in the line |
| 52 | + * @param getFillColor |
| 53 | + * @returns |
| 54 | + */ |
| 55 | +function getLineColorFromFillColor(getFillColor: TrajectoryTileLayerProps['getFillColor']) { |
| 56 | + return (d: any, info: any) => { |
| 57 | + const {index, data, target} = info; |
| 58 | + const startIndex = data.startIndices[index]; |
| 59 | + const endIndex = data.startIndices[index + 1]; |
| 60 | + const nVertices = endIndex - startIndex; |
| 61 | + const colors = new Array(nVertices).fill(0).map((_, i) => { |
| 62 | + const vertexIndex = startIndex + i; |
| 63 | + const properties = createBinaryProxy(data, vertexIndex); |
| 64 | + const vertex = {properties} as any; |
| 65 | + return typeof getFillColor === 'function' |
| 66 | + ? getFillColor(vertex, {index: vertexIndex, data, target}) |
| 67 | + : getFillColor; |
| 68 | + }); |
| 69 | + |
| 70 | + return colors; |
| 71 | + }; |
| 72 | +} |
| 73 | + |
| 74 | +// @ts-ignore |
| 75 | +export default class TrajectoryTileLayer< |
| 76 | + FeaturePropertiesT = any, |
| 77 | + ExtraProps extends {} = {} |
| 78 | +> extends VectorTileLayer<FeaturePropertiesT, Required<_TrajectoryTileLayerProps> & ExtraProps> { |
| 79 | + static layerName = 'TrajectoryTileLayer'; |
| 80 | + static defaultProps = defaultProps; |
| 81 | + |
| 82 | + state!: VectorTileLayer['state'] & { |
| 83 | + trajectoryIdColumn: string; |
| 84 | + timestampColumn: string; |
| 85 | + minTime: number; |
| 86 | + maxTime: number; |
| 87 | + }; |
| 88 | + |
| 89 | + constructor(...propObjects: TrajectoryTileLayerProps<FeaturePropertiesT>[]) { |
| 90 | + // @ts-ignore |
| 91 | + super(...propObjects); |
| 92 | + } |
| 93 | + |
| 94 | + updateState(parameters) { |
| 95 | + super.updateState(parameters); |
| 96 | + if (parameters.props.data && parameters.props.data.widgetSource) { |
| 97 | + const dataSourceProps = parameters.props.data.widgetSource.props; |
| 98 | + const {trajectoryIdColumn, timestampColumn} = dataSourceProps; |
| 99 | + |
| 100 | + if (!trajectoryIdColumn) { |
| 101 | + throw new Error( |
| 102 | + 'TrajectoryTileLayer: trajectoryIdColumn is required in data source configuration' |
| 103 | + ); |
| 104 | + } |
| 105 | + if (!timestampColumn) { |
| 106 | + throw new Error( |
| 107 | + 'TrajectoryTileLayer: timestampColumn is required in data source configuration' |
| 108 | + ); |
| 109 | + } |
| 110 | + |
| 111 | + this.setState({trajectoryIdColumn, timestampColumn}); |
| 112 | + } |
| 113 | + |
| 114 | + // Read timestampRange from the data source (tilejson) |
| 115 | + if (parameters.props.data && parameters.props.data.timestampRange) { |
| 116 | + const {min, max} = parameters.props.data.timestampRange; |
| 117 | + const minTime = normalizeTimestamp(min); |
| 118 | + const maxTime = normalizeTimestamp(max); |
| 119 | + this.setState({minTime, maxTime}); |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + async getTileData(tile) { |
| 124 | + const data = await super.getTileData(tile); |
| 125 | + if (!data || !data.points) return data; |
| 126 | + |
| 127 | + // Get tile bounds from the tile object |
| 128 | + const tileBounds = tile.bbox as TileBounds; |
| 129 | + const {minTime, maxTime} = this.state; |
| 130 | + |
| 131 | + const lines = transformTrajectoryData( |
| 132 | + data.points, |
| 133 | + this.state.trajectoryIdColumn, |
| 134 | + this.state.timestampColumn, |
| 135 | + tileBounds, |
| 136 | + {min: minTime, max: maxTime} |
| 137 | + ); |
| 138 | + |
| 139 | + if (!lines) return null; |
| 140 | + if (this.props.autocomputeSpeed) { |
| 141 | + autocomputeSpeed(lines); |
| 142 | + } |
| 143 | + return {...createEmptyBinary(), lines}; |
| 144 | + } |
| 145 | + |
| 146 | + renderSubLayers( |
| 147 | + props: TrajectoryTileLayerProps & { |
| 148 | + id: string; |
| 149 | + data: any; |
| 150 | + _offset: number; |
| 151 | + tile: _Tile2DHeader; |
| 152 | + _subLayerProps: CompositeLayerProps['_subLayerProps']; |
| 153 | + } |
| 154 | + ): GeoJsonLayer | GeoJsonLayer[] | null { |
| 155 | + if (props.data === null) { |
| 156 | + return null; |
| 157 | + } |
| 158 | + |
| 159 | + // This may not be as efficient as just rendering a PathLayer, but it allows to |
| 160 | + // switch between the render modes without reloading data |
| 161 | + const showTrips = props.renderMode === 'trips'; |
| 162 | + |
| 163 | + // Normalize currentTime to match the normalized timestamps in the data |
| 164 | + const normalizedCurrentTime = props.currentTime! - this.state.minTime; |
| 165 | + const {minTime, maxTime} = this.state; |
| 166 | + const totalTimeSpan = maxTime - minTime; |
| 167 | + |
| 168 | + const layerProps = { |
| 169 | + getWidth: props.getWidth, |
| 170 | + widthUnits: props.widthUnits || 'pixels', |
| 171 | + lineJointRounded: props.jointRounded !== undefined ? props.jointRounded : true, |
| 172 | + capRounded: props.capRounded !== undefined ? props.capRounded : true, |
| 173 | + _pathType: props._pathType || 'open' |
| 174 | + }; |
| 175 | + |
| 176 | + const getLineColor = props.getFillColor |
| 177 | + ? getLineColorFromFillColor(props.getFillColor) |
| 178 | + : props.getLineColor; |
| 179 | + |
| 180 | + const modifiedProps = { |
| 181 | + ...props, |
| 182 | + getLineColor, |
| 183 | + _subLayerProps: { |
| 184 | + ...props._subLayerProps, |
| 185 | + linestrings: { |
| 186 | + type: TripsLayer, |
| 187 | + currentTime: showTrips ? normalizedCurrentTime : totalTimeSpan, |
| 188 | + trailLength: showTrips ? props.trailLength : totalTimeSpan, |
| 189 | + parameters: {depthTest: false}, |
| 190 | + ...layerProps, |
| 191 | + ...props._subLayerProps?.linestrings |
| 192 | + } |
| 193 | + } |
| 194 | + }; |
| 195 | + |
| 196 | + return super.renderSubLayers(modifiedProps as any); |
| 197 | + } |
| 198 | +} |
0 commit comments