Skip to content

Commit b3494ce

Browse files
committed
CARTO: Add TrajectoryTileLayer
1 parent 0d3c3b3 commit b3494ce

File tree

16 files changed

+1655
-10
lines changed

16 files changed

+1655
-10
lines changed

modules/carto/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"prepublishOnly": "npm run build-bundle && npm run build-bundle -- --env=dev"
4343
},
4444
"dependencies": {
45-
"@carto/api-client": "^0.5.6",
45+
"@carto/api-client": "^0.5.15",
4646
"@loaders.gl/compression": "^4.2.0",
4747
"@loaders.gl/gis": "^4.2.0",
4848
"@loaders.gl/loader-utils": "^4.2.0",

modules/carto/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {default as HeatmapTileLayer} from './layers/heatmap-tile-layer';
99
import {default as PointLabelLayer} from './layers/point-label-layer';
1010
import {default as QuadbinTileLayer} from './layers/quadbin-tile-layer';
1111
import {default as RasterTileLayer} from './layers/raster-tile-layer';
12+
import {default as TrajectoryTileLayer} from './layers/trajectory-tile-layer';
1213
import {default as VectorTileLayer} from './layers/vector-tile-layer';
1314

1415
// Exports for playground/bindings
@@ -19,6 +20,7 @@ const CARTO_LAYERS = {
1920
PointLabelLayer,
2021
QuadbinTileLayer,
2122
RasterTileLayer,
23+
TrajectoryTileLayer,
2224
VectorTileLayer
2325
};
2426
export {
@@ -29,6 +31,7 @@ export {
2931
PointLabelLayer,
3032
QuadbinTileLayer,
3133
RasterTileLayer,
34+
TrajectoryTileLayer,
3235
VectorTileLayer
3336
};
3437

@@ -47,6 +50,7 @@ export type {QuadbinTileLayerProps} from './layers/quadbin-tile-layer';
4750
export type {RasterLayerProps} from './layers/raster-layer';
4851
export type {RasterTileLayerProps} from './layers/raster-tile-layer';
4952
export type {SpatialIndexTileLayerProps} from './layers/spatial-index-tile-layer';
53+
export type {TrajectoryTileLayerProps} from './layers/trajectory-tile-layer';
5054
export type {VectorTileLayerProps} from './layers/vector-tile-layer';
5155

5256
// Helpers
@@ -63,6 +67,7 @@ export {default as colorCategories} from './style/color-categories-style';
6367
export {default as colorContinuous} from './style/color-continuous-style';
6468
export {fetchMap, LayerFactory} from './api/fetch-map';
6569
export {fetchBasemapProps} from './api/basemap';
70+
export {normalizeTimestamp} from './layers/trajectory-utils';
6671
export type {
6772
FetchMapOptions,
6873
FetchMapResult,
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// deck.gl
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (c) vis.gl contributors
4+
5+
import type {ProcessedGeometry} from './trajectory-utils';
6+
7+
// Helper functions for speed calculation
8+
function deg2rad(deg: number): number {
9+
return deg * (Math.PI / 180);
10+
}
11+
12+
function distanceBetweenPoints([lon1, lat1, lon2, lat2]: number[]): number {
13+
const R = 6371000; // Radius of the earth in m
14+
const dLat = deg2rad(lat2 - lat1);
15+
const dLon = deg2rad(lon2 - lon1);
16+
const a =
17+
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
18+
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
19+
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
20+
}
21+
22+
/**
23+
* Calculate speeds and write to numericProps attribute for trajectory geometry
24+
*/
25+
export function autocomputeSpeed(geometry: ProcessedGeometry): void {
26+
const {positions, attributes} = geometry;
27+
const n = positions.value.length / positions.size;
28+
geometry.numericProps.speed = {value: new Float32Array(n), size: 1};
29+
30+
// Calculate speed and write to numericProps
31+
let previousSpeed = 0;
32+
33+
for (let i = 0; i < n; i++) {
34+
let speed = 0;
35+
36+
if (i < n - 1) {
37+
const start = i === n - 1 ? i - 1 : i;
38+
const step = positions.value.subarray(
39+
positions.size * start,
40+
positions.size * start + 2 * positions.size
41+
);
42+
let lat1: number = 0;
43+
let lat2: number = 0;
44+
let lon1: number = 0;
45+
let lon2: number = 0;
46+
47+
if (positions.size === 2) {
48+
[lon1, lat1, lon2, lat2] = step;
49+
} else if (positions.size === 3) {
50+
[lon1, lat1, , lon2, lat2] = step; // skip altitude values
51+
}
52+
53+
const deltaP = distanceBetweenPoints([lon1, lat1, lon2, lat2]);
54+
const [t1, t2] = attributes.getTimestamps.value.subarray(start, start + 2);
55+
const deltaT = t2 - t1;
56+
speed = deltaT > 0 ? deltaP / deltaT : previousSpeed;
57+
58+
if (deltaT === 0) {
59+
speed = previousSpeed;
60+
}
61+
62+
previousSpeed = speed;
63+
}
64+
65+
if (speed === 0) {
66+
speed = 100; // fallback speed
67+
}
68+
69+
// Write speed to numericProps
70+
geometry.numericProps.speed.value[i] = speed;
71+
}
72+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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

Comments
 (0)