Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: path distance picking extension #205

Merged
merged 10 commits into from
Jan 28, 2025
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