Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/carto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"prepublishOnly": "npm run build-bundle && npm run build-bundle -- --env=dev"
},
"dependencies": {
"@carto/api-client": "^0.5.6",
"@carto/api-client": "^0.5.15",
"@loaders.gl/compression": "^4.2.0",
"@loaders.gl/gis": "^4.2.0",
"@loaders.gl/loader-utils": "^4.2.0",
Expand Down
5 changes: 5 additions & 0 deletions modules/carto/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {default as HeatmapTileLayer} from './layers/heatmap-tile-layer';
import {default as PointLabelLayer} from './layers/point-label-layer';
import {default as QuadbinTileLayer} from './layers/quadbin-tile-layer';
import {default as RasterTileLayer} from './layers/raster-tile-layer';
import {default as TrajectoryTileLayer} from './layers/trajectory-tile-layer';
import {default as VectorTileLayer} from './layers/vector-tile-layer';

// Exports for playground/bindings
Expand All @@ -19,6 +20,7 @@ const CARTO_LAYERS = {
PointLabelLayer,
QuadbinTileLayer,
RasterTileLayer,
TrajectoryTileLayer,
VectorTileLayer
};
export {
Expand All @@ -29,6 +31,7 @@ export {
PointLabelLayer,
QuadbinTileLayer,
RasterTileLayer,
TrajectoryTileLayer,
VectorTileLayer
};

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

// Helpers
Expand All @@ -63,6 +67,7 @@ export {default as colorCategories} from './style/color-categories-style';
export {default as colorContinuous} from './style/color-continuous-style';
export {fetchMap, LayerFactory} from './api/fetch-map';
export {fetchBasemapProps} from './api/basemap';
export {normalizeTimestamp} from './layers/trajectory-utils';
export type {
FetchMapOptions,
FetchMapResult,
Expand Down
72 changes: 72 additions & 0 deletions modules/carto/src/layers/trajectory-speed-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import type {ProcessedGeometry} from './trajectory-utils';

// Helper functions for speed calculation
function deg2rad(deg: number): number {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: There's toRadians in @math.gl/core, but it doesn't seem to be used elsewhere in the codebase so no strong preference here.

return deg * (Math.PI / 180);
}

function distanceBetweenPoints([lon1, lat1, lon2, lat2]: number[]): number {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the units of the return value of distanceBetweenPoints? Might also be a good addition to @math.gl/* at some point.

const R = 6371000; // Radius of the earth in m
const dLat = deg2rad(lat2 - lat1);
const dLon = deg2rad(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

/**
* Calculate speeds and write to numericProps attribute for trajectory geometry
*/
export function autocomputeSpeed(geometry: ProcessedGeometry): void {
const {positions, attributes} = geometry;
const n = positions.value.length / positions.size;
geometry.numericProps.speed = {value: new Float32Array(n), size: 1};

// Calculate speed and write to numericProps
let previousSpeed = 0;

for (let i = 0; i < n; i++) {
let speed = 0;

if (i < n - 1) {
const start = i === n - 1 ? i - 1 : i;
const step = positions.value.subarray(
positions.size * start,
positions.size * start + 2 * positions.size
);
let lat1: number = 0;
let lat2: number = 0;
let lon1: number = 0;
let lon2: number = 0;

if (positions.size === 2) {
[lon1, lat1, lon2, lat2] = step;
} else if (positions.size === 3) {
[lon1, lat1, , lon2, lat2] = step; // skip altitude values
}

const deltaP = distanceBetweenPoints([lon1, lat1, lon2, lat2]);
const [t1, t2] = attributes.getTimestamps.value.subarray(start, start + 2);
const deltaT = t2 - t1;
speed = deltaT > 0 ? deltaP / deltaT : previousSpeed;

if (deltaT === 0) {
speed = previousSpeed;
}

previousSpeed = speed;
}

if (speed === 0) {
speed = 100; // fallback speed
}

// Write speed to numericProps
geometry.numericProps.speed.value[i] = speed;
}
}
198 changes: 198 additions & 0 deletions modules/carto/src/layers/trajectory-tile-layer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {CompositeLayerProps, DefaultProps} from '@deck.gl/core';
import {_Tile2DHeader, TripsLayer, TripsLayerProps} from '@deck.gl/geo-layers';
import {GeoJsonLayer} from '@deck.gl/layers';

import type {TilejsonResult} from '@carto/api-client';
import VectorTileLayer, {VectorTileLayerProps} from './vector-tile-layer';
import {transformTrajectoryData, type TileBounds, normalizeTimestamp} from './trajectory-utils';
import {autocomputeSpeed} from './trajectory-speed-utils';
import {createBinaryProxy, createEmptyBinary} from '../utils';

const defaultProps: DefaultProps<TrajectoryTileLayerProps> = {
...TripsLayer.defaultProps,
data: VectorTileLayer.defaultProps.data,
autocomputeSpeed: false,
renderMode: 'trips',
currentTime: 0,
trailLength: 1000
};

/** All properties supported by TrajectoryTileLayer. */
export type TrajectoryTileLayerProps<FeaturePropertiesT = unknown> = _TrajectoryTileLayerProps &
Omit<TripsLayerProps<FeaturePropertiesT>, 'data'> &
Pick<
VectorTileLayerProps<FeaturePropertiesT>,
'getFillColor' | 'getLineColor' | 'uniqueIdProperty'
>;

/** Properties added by TrajectoryTileLayer. */
type _TrajectoryTileLayerProps = {
data: null | TilejsonResult | Promise<TilejsonResult>;

/**
* Set to true to automatically compute speed for each vertex and store in properties.speed
*/
autocomputeSpeed?: boolean;

/**
* Rendering mode for trajectories.
* - 'paths': Static path rendering
* - 'trips': Animated trip rendering with time controls
*/
renderMode?: 'paths' | 'trips';
Comment on lines +41 to +46
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: I slightly prefer your static | animated naming from the comment over paths | trips, I think it might be clearer.

};

/**
* Helper function to wrap `getFillColor` accessor into a `getLineColor` accessor
* which will invoke `getFillColor` for each vertex in the line
* @param getFillColor
* @returns
*/
function getLineColorFromFillColor(getFillColor: TrajectoryTileLayerProps['getFillColor']) {
return (d: any, info: any) => {
const {index, data, target} = info;
const startIndex = data.startIndices[index];
const endIndex = data.startIndices[index + 1];
const nVertices = endIndex - startIndex;
const colors = new Array(nVertices).fill(0).map((_, i) => {
const vertexIndex = startIndex + i;
const properties = createBinaryProxy(data, vertexIndex);
const vertex = {properties} as any;
return typeof getFillColor === 'function'
? getFillColor(vertex, {index: vertexIndex, data, target})
: getFillColor;
});

return colors;
};
}

// @ts-ignore
export default class TrajectoryTileLayer<
FeaturePropertiesT = any,
ExtraProps extends {} = {}
> extends VectorTileLayer<FeaturePropertiesT, Required<_TrajectoryTileLayerProps> & ExtraProps> {
static layerName = 'TrajectoryTileLayer';
static defaultProps = defaultProps;

state!: VectorTileLayer['state'] & {
trajectoryIdColumn: string;
timestampColumn: string;
minTime: number;
maxTime: number;
};

constructor(...propObjects: TrajectoryTileLayerProps<FeaturePropertiesT>[]) {
// @ts-ignore
super(...propObjects);
}

updateState(parameters) {
super.updateState(parameters);
if (parameters.props.data && parameters.props.data.widgetSource) {
const dataSourceProps = parameters.props.data.widgetSource.props;
const {trajectoryIdColumn, timestampColumn} = dataSourceProps;

if (!trajectoryIdColumn) {
throw new Error(
'TrajectoryTileLayer: trajectoryIdColumn is required in data source configuration'
);
}
if (!timestampColumn) {
throw new Error(
'TrajectoryTileLayer: timestampColumn is required in data source configuration'
);
}
Comment on lines +100 to +109
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional - sliiightly more concise/readable with an assert() here maybe?


this.setState({trajectoryIdColumn, timestampColumn});
}

// Read timestampRange from the data source (tilejson)
if (parameters.props.data && parameters.props.data.timestampRange) {
const {min, max} = parameters.props.data.timestampRange;
const minTime = normalizeTimestamp(min);
const maxTime = normalizeTimestamp(max);
this.setState({minTime, maxTime});
}
}

async getTileData(tile) {
const data = await super.getTileData(tile);
if (!data || !data.points) return data;

// Get tile bounds from the tile object
const tileBounds = tile.bbox as TileBounds;
const {minTime, maxTime} = this.state;

const lines = transformTrajectoryData(
data.points,
this.state.trajectoryIdColumn,
this.state.timestampColumn,
tileBounds,
{min: minTime, max: maxTime}
);

if (!lines) return null;
if (this.props.autocomputeSpeed) {
autocomputeSpeed(lines);
}
return {...createEmptyBinary(), lines};
}

renderSubLayers(
props: TrajectoryTileLayerProps & {
id: string;
data: any;
_offset: number;
tile: _Tile2DHeader;
_subLayerProps: CompositeLayerProps['_subLayerProps'];
}
): GeoJsonLayer | GeoJsonLayer[] | null {
if (props.data === null) {
return null;
}

// This may not be as efficient as just rendering a PathLayer, but it allows to
// switch between the render modes without reloading data
const showTrips = props.renderMode === 'trips';

// Normalize currentTime to match the normalized timestamps in the data
const normalizedCurrentTime = props.currentTime! - this.state.minTime;
const {minTime, maxTime} = this.state;
const totalTimeSpan = maxTime - minTime;

const layerProps = {
getWidth: props.getWidth,
widthUnits: props.widthUnits || 'pixels',
lineJointRounded: props.jointRounded !== undefined ? props.jointRounded : true,
capRounded: props.capRounded !== undefined ? props.capRounded : true,
_pathType: props._pathType || 'open'
};

const getLineColor = props.getFillColor
? getLineColorFromFillColor(props.getFillColor)
: props.getLineColor;

const modifiedProps = {
...props,
getLineColor,
_subLayerProps: {
...props._subLayerProps,
linestrings: {
type: TripsLayer,
currentTime: showTrips ? normalizedCurrentTime : totalTimeSpan,
trailLength: showTrips ? props.trailLength : totalTimeSpan,
parameters: {depthTest: false},
...layerProps,
...props._subLayerProps?.linestrings
}
}
};

return super.renderSubLayers(modifiedProps as any);
}
}
Loading
Loading