Skip to content

Commit

Permalink
feat: path distance picking extension (#205)
Browse files Browse the repository at this point in the history
  • Loading branch information
hkfb authored Jan 28, 2025
1 parent 61b1c75 commit f2e7ef5
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ React/Typescript library for interactive 3D visualization of GPX and TCX activit

## Features
* [Deck.gl](https://deck.gl/) [layers](./src/layers/README.md) for visualization of GPX and TCX activities.
* [Deck.gl](https://deck.gl/) [layer extensions](./src/extensions/README.md) for path type layers
* Map components
* Activity Map - renders a GPX activity
* Focus Activity Map - renders a GPX trace and automatically centers the camera around the bounds of the displayed trace
Expand Down
Binary file modified __snapshots__/chromium_components-activity-maptiler--default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/chromium_layers-activity-layer--picking.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/webkit_components-activity-maptiler--default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/webkit_layers-activity-layer--picking.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/extensions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Deck.gl layer extensions

[Deck.gl](https://deck.gl/) layers extensions for visualization of GPX and TCX activities.

## Path Texture Extension
Colors a path using a texture.

## Path Distance Picking Extension
Allows picking the distance along a Path layer.
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ExtrudedPathLayer } from "../../layers/extruded-path-layer/extruded-path-layer";
import { DeckGL } from "@deck.gl/react";
import * as React from "react";
import type { StoryObj } from "@storybook/react";
import { PathDistancePickingExtension } from "./path-distance-picking-extension";
import * as _ from "lodash";
import { MapViewState, type PickingInfo } from "@deck.gl/core";
import { StreetLayer } from "../../layers/street-layer";
import { SYNTHETIC_DATA } from "../../constant.stories";
import { userEvent, fireEvent } from "@storybook/test";

export default {
title: "Extensions / Path Distance Picking Extension",
tags: ["autodocs"],
};

const DEFAULT_PROPS = {
id: "extruded-path-layer",
getWidth: 3000,
extensions: [new PathDistancePickingExtension()],
pickable: true,
data: SYNTHETIC_DATA,
};

export const PathDistancePicking: StoryObj = {
render: () => {
const layer = new ExtrudedPathLayer({ ...DEFAULT_PROPS });

const initialViewState: MapViewState = {
zoom: 8,
latitude: 62.1,
longitude: 8.7,
pitch: 60,
};

const getTooltip = React.useCallback(
({ coordinate, picked, index }: PickingInfo) => {
if (!coordinate || _.isEmpty(coordinate) || !picked) {
return null;
}

const latTrunc = Math.floor(coordinate[1]);
const lonTrunc = Math.floor(coordinate[0]);

return {
html: `lat: ${latTrunc} lon: ${lonTrunc} distance: ${index}`,
};
},
[],
);

const base = new StreetLayer();

return (
<DeckGL
layers={[base, layer]}
initialViewState={initialViewState}
controller
getTooltip={getTooltip}
></DeckGL>
);
},
parameters: {
docs: {
description: {
story: "Pick the distance along the path.",
},
},
},
play: async () => {
const delay = 300;
const canvas = document.querySelector("canvas");

if (!canvas) {
return;
}

await userEvent.click(canvas, { delay });
await userEvent.hover(canvas, { delay });
await fireEvent.mouseMove(canvas, {
clientX: canvas.width / 2,
clientY: canvas.height / 2,
delay,
});
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"use strict";

import { Layer, LayerExtension, LayerContext, picking } from "@deck.gl/core";
import { vec3 } from "@math.gl/core";

/**
* A deck.gl layer extension for picking the distance along a path.
*/
export class PathDistancePickingExtension extends LayerExtension {
static extensionName = "PathDistancePickingExtension";
static defaultProps = {};

/**
* Called once when the layer is initialized. We add the `customPickingColors`
*/
override initializeState(
this: Layer,
context: LayerContext,
extension: this,
) {
const attributeManager = this.getAttributeManager();
if (!attributeManager) return;

attributeManager.addInstanced({
instanceDistAlongPath: {
size: 1,
accessor: "getPath",
transform: (path) =>
extension._getPerVertexDistances(path, this),
},
});
}

private _getPerVertexDistances(path: number[][], layer: Layer): number[] {
if (!Array.isArray(path) || path.length < 2) {
// Degenerate path -> single vertex, distance = 0
return [0, 0, 0];
}

// Project each [longitude, latitude] or [x, y]
const projectedPositions: [number, number, number][] = [];
for (const pt of path) {
const [x, y] = layer.projectPosition(pt);
projectedPositions.push([x, y, 0]);
}

// Compute cumulative distances in projected space
const distances = [0];
let totalDist = 0;
for (let i = 1; i < projectedPositions.length; i++) {
totalDist += vec3.dist(
projectedPositions[i],
projectedPositions[i - 1],
);
distances.push(totalDist);
}

return distances;
}

override getShaders() {
return {
name: "path-distance-picking-extension",
dependencies: [picking],
inject: {
// Vertex shader: declare and set the picking color
"vs:#decl": `
in float instanceDistAlongPath;
`,
"vs:#main-end": `
vec3 segmentStart = project_position(instanceStartPositions, instanceStartPositions64Low);
vec3 segmentEnd = project_position(instanceEndPositions, instanceEndPositions64Low);
vec3 deltaCommon = segmentEnd - segmentStart;
float segmentUnits = length(deltaCommon.xy);
float distUnits = mix(instanceDistAlongPath, instanceDistAlongPath + segmentUnits, isEnd);
float distMeters = distUnits / project_uCommonUnitsPerMeter.z / project_size();
picking_vRGBcolor_Avalid.r = distMeters;
`,
"fs:#decl": `
// Encode a float distance (0..16777215) into an RGB color.
vec3 encodeDistanceToRGB(float distance) {
float distClamped = clamp(distance, 0.0, 16777215.0);
int distInt = int(floor(distClamped + 0.5));
int r = distInt & 0xFF; // low byte
int g = (distInt >> 8) & 0xFF; // mid byte
int b = (distInt >> 16) & 0xFF; // high byte
return vec3(float(r + 1), float(g), float(b)) / 255.;
}
`,
"fs:#main-end": `
if (bool(picking.isActive)) {
fragColor.rgb = encodeDistanceToRGB(picking_vRGBcolor_Avalid.r);
}
`,
},
};
}
}
2 changes: 1 addition & 1 deletion src/layers/activity-layer/activity-layer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ export const Picking: StoryObj = {
return null;
}
return {
html: `<p>lat: ${coordinate[1]}, lon: ${coordinate[0]} </p>`,
html: `lat: ${coordinate[1]}, lon: ${coordinate[0]}`,
};
}, []);

Expand Down

0 comments on commit f2e7ef5

Please sign in to comment.