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

slider + spacetime #756

Merged
merged 4 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 11 additions & 2 deletions ui-core/src/components/inputs/Slider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import type { ChangeEvent, MouseEvent, InputHTMLAttributes } from 'react';

import cx from 'classnames';

export type SliderProps = InputHTMLAttributes<HTMLInputElement> & {
width?: number;
onChangeCommitted?: (e: MouseEvent<HTMLInputElement>) => void;
containerClassName?: string;
};

// onChange returns an event or number
Expand All @@ -19,12 +20,17 @@ const Slider = ({
onChange,
onChangeCommitted,
disabled,
containerClassName,
...rest
}: SliderProps) => {
const [value, setValue] = useState<number>(
initialValue !== undefined ? Number(initialValue) : Number(min)
);

useEffect(() => {
setValue(Number(initialValue));
}, [initialValue]);

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const newValue = Number(e.target.value);
setValue(newValue);
Expand All @@ -38,7 +44,10 @@ const Slider = ({
};

return (
<div className={cx('range-wrapper', { disabled })} style={{ width: `${width}px` }}>
<div
className={cx('range-wrapper', containerClassName, { disabled })}
style={{ width: `${width}px` }}
>
<input
type="range"
className="range-slider"
Expand Down
1 change: 1 addition & 0 deletions ui-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export { default as RadioGroup, RadioGroupProps } from './components/inputs/Radi
export { default as Select, SelectProps } from './components/Select';
export { default as TextArea, TextAreaProps } from './components/inputs/TextArea';
export { default as TimePicker } from './components/inputs/TimePicker';
export { default as Slider } from './components/inputs/Slider';
export {
default as TolerancePicker,
type TolerancePickerProps,
Expand Down
54 changes: 1 addition & 53 deletions ui-manchette-with-spacetimechart/src/__tests__/helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { describe, it, expect } from 'vitest';

import { BASE_WAYPOINT_HEIGHT, MAX_TIME_WINDOW } from '../consts';
import { BASE_WAYPOINT_HEIGHT } from '../consts';
import {
calcWaypointsToDisplay,
calcWaypointsHeight,
computeTimeWindow,
getWaypointsWithPosition,
getScales,
} from '../helpers';
Expand Down Expand Up @@ -89,57 +88,6 @@ describe('calcWaypointsHeight', () => {
});
});

// Tests for computeTimeWindow
describe('computeTimeWindow', () => {
const mockTrains = [
{
departureTime: new Date('2023-01-01T00:00:00Z'),
spaceTimeCurves: [
{
times: [0, 10000],
positions: [],
},
],
name: '',
id: 0,
},
{
departureTime: new Date('2023-01-01T01:00:00Z'),
spaceTimeCurves: [
{
times: [0, 20000],
positions: [],
},
],
name: '',
id: 0,
},
];

it('should calculate the correct time window', () => {
const timeWindow = computeTimeWindow(mockTrains);
expect(timeWindow).toBeLessThanOrEqual(MAX_TIME_WINDOW);
});

it('should return MAX_TIME_WINDOW if calculated time is too large', () => {
const mockLongTrains = [
{
departureTime: new Date('2023-01-01T00:00:00Z'),
spaceTimeCurves: [
{
times: [0, 100000000],
positions: [],
},
],
name: '',
id: 0,
},
];
const timeWindow: number = computeTimeWindow(mockLongTrains);
expect(timeWindow).toBe(MAX_TIME_WINDOW);
});
});

describe('getWaypointsWithPosition', () => {
it('should return waypoints with position and label', () => {
const result = getWaypointsWithPosition(mockedWaypoints);
Expand Down
9 changes: 5 additions & 4 deletions ui-manchette-with-spacetimechart/src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ export const BASE_WAYPOINT_HEIGHT = 32;
export const INITIAL_WAYPOINT_LIST_HEIGHT = 521;
export const INITIAL_SPACE_TIME_CHART_HEIGHT = INITIAL_WAYPOINT_LIST_HEIGHT + 40;

export const MIN_ZOOM_X = 1;
export const MAX_ZOOM_X = 10;
export const ZOOM_X_DELTA = 0.5;
export const MIN_ZOOM_MS_PER_PX = 600000;
export const MAX_ZOOM_MS_PER_PX = 625;
export const DEFAULT_ZOOM_MS_PER_PX = 7500;
export const MIN_ZOOM_X = 0;
export const MAX_ZOOM_X = 100;

export const MIN_ZOOM_Y = 1;
export const MAX_ZOOM_Y = 10.5;
export const ZOOM_Y_DELTA = 0.5;

export const FOOTER_HEIGHT = 40; // height of the manchette footer
export const WAYPOINT_LINE_HEIGHT = 16;
export const MAX_TIME_WINDOW = 60 * 60 * 12; // 12 hours
71 changes: 29 additions & 42 deletions ui-manchette-with-spacetimechart/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type {
InteractiveWaypoint,
ProjectPathTrainResult,
Waypoint,
} from '@osrd-project/ui-manchette/dist/types';
import type {
OperationalPoint,
SpaceTimeChartProps,
} from '@osrd-project/ui-spacetimechart/dist/lib/types';

import { BASE_WAYPOINT_HEIGHT, MAX_TIME_WINDOW, MAX_ZOOM_X, MIN_ZOOM_X } from './consts';
import { calcTotalDistance, getHeightWithoutLastWaypoint, msToS } from './utils';
import type { InteractiveWaypoint, Waypoint } from '@osrd-project/ui-manchette/dist/types';
import type { OperationalPoint } from '@osrd-project/ui-spacetimechart/dist/lib/types';
import { clamp } from 'lodash';

import {
BASE_WAYPOINT_HEIGHT,
MAX_ZOOM_MS_PER_PX,
MAX_ZOOM_X,
MIN_ZOOM_MS_PER_PX,
MIN_ZOOM_X,
} from './consts';
import { calcTotalDistance, getHeightWithoutLastWaypoint } from './utils';

type WaypointsOptions = { isProportional: boolean; yZoom: number; height: number };

Expand Down Expand Up @@ -87,28 +87,6 @@ export const calcWaypointsHeight = (
});
};

export const computeTimeWindow = (trains: ProjectPathTrainResult[]) => {
const { minTime, maxTime } = trains.reduce(
(times, train) => {
if (train.spaceTimeCurves.length === 0) return times;

const lastCurve = train.spaceTimeCurves.at(-1);
if (!lastCurve || lastCurve.times.length < 2) return times;

const firstPoint = Number(train.departureTime);
const lastPoint = Number(train.departureTime) + lastCurve.times.at(-1)!;
return {
minTime: times.minTime === -1 || times.minTime > firstPoint ? firstPoint : times.minTime,
maxTime: times.maxTime === -1 || times.maxTime < lastPoint ? lastPoint : times.maxTime,
};
},
{ minTime: -1, maxTime: -1 }
);

const timeWindow = msToS(maxTime - minTime);
return timeWindow > MAX_TIME_WINDOW ? MAX_TIME_WINDOW : timeWindow;
};

export const getWaypointsWithPosition = (waypoints: InteractiveWaypoint[]): OperationalPoint[] =>
waypoints.map((point) => ({
id: point.id,
Expand Down Expand Up @@ -154,17 +132,26 @@ export const getScales = (
];
};

export const zoomValueToTimeScale = (slider: number) =>
MIN_ZOOM_MS_PER_PX * Math.pow(MAX_ZOOM_MS_PER_PX / MIN_ZOOM_MS_PER_PX, slider / 100);

export const timeScaleToZoomValue = (timeScale: number) =>
(100 * Math.log(timeScale / MIN_ZOOM_MS_PER_PX)) /
Math.log(MAX_ZOOM_MS_PER_PX / MIN_ZOOM_MS_PER_PX);

/** Zoom on X axis and center on the mouse position */
export const zoomX = (
currentXZoom: number,
currentXOffset: number,
{ delta, position: { x } }: Parameters<NonNullable<SpaceTimeChartProps['onZoom']>>[0]
currentZoom: number,
currentOffset: number,
newZoom: number,
position: number
) => {
const xZoom = Math.min(Math.max(currentXZoom * (1 + delta / 10), MIN_ZOOM_X), MAX_ZOOM_X);
const boundedZoom = clamp(newZoom, MIN_ZOOM_X, MAX_ZOOM_X);
const oldTimeScale = zoomValueToTimeScale(currentZoom);
const newTimeScale = zoomValueToTimeScale(boundedZoom);
const newOffset = position - ((position - currentOffset) * oldTimeScale) / newTimeScale;
return {
xZoom,
// Adjust zoom level relatively to the input delta value:
// These lines are here to center the zoom on the mouse position:
xOffset: x - ((x - currentXOffset) / currentXZoom) * xZoom,
xZoom: boundedZoom,
xOffset: newOffset,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ import type {
} from '@osrd-project/ui-spacetimechart/dist/lib/types';

import usePaths from './usePaths';
import { MAX_ZOOM_Y, MIN_ZOOM_Y, ZOOM_Y_DELTA, INITIAL_SPACE_TIME_CHART_HEIGHT } from '../consts';
import {
MAX_ZOOM_Y,
MIN_ZOOM_Y,
ZOOM_Y_DELTA,
INITIAL_SPACE_TIME_CHART_HEIGHT,
DEFAULT_ZOOM_MS_PER_PX,
} from '../consts';
import {
calcWaypointsToDisplay,
computeTimeWindow,
getWaypointsWithPosition as getOperationalPointWithPosition,
getScales,
calcWaypointsHeight,
zoomX,
zoomValueToTimeScale,
timeScaleToZoomValue,
} from '../helpers';
import { getDiff } from '../utils/point';

Expand All @@ -35,11 +42,12 @@ const useManchettesWithSpaceTimeChart = (
projectPathTrainResult: ProjectPathTrainResult[],
manchetteWithSpaceTimeChartContainer: React.RefObject<HTMLDivElement>,
selectedTrain?: number,
height = 561
height = 561,
spaceTimeChartRef?: React.RefObject<HTMLDivElement>
) => {
const [isShiftPressed, setIsShiftPressed] = useState(false);
const [state, setState] = useState<State>({
xZoom: 1,
xZoom: timeScaleToZoomValue(DEFAULT_ZOOM_MS_PER_PX),
yZoom: 1,
xOffset: 0,
yOffset: 0,
Expand All @@ -54,12 +62,6 @@ const useManchettesWithSpaceTimeChart = (

const paths = usePaths(projectPathTrainResult, selectedTrain);

// Memoize timeWindow to avoid recalculation on each render
const timeWindow = useMemo(
() => computeTimeWindow(projectPathTrainResult),
[projectPathTrainResult]
);

const waypointsToDisplay = useMemo(
() => calcWaypointsToDisplay(waypoints, { height, isProportional, yZoom }),
[waypoints, height, isProportional, yZoom]
Expand Down Expand Up @@ -130,7 +132,6 @@ const useManchettesWithSpaceTimeChart = (
[operationalPointsWithPosition, height, isProportional, yZoom]
);

// Memoize manchetteProps separately
const manchetteProps = useMemo(
() => ({
waypoints: waypointWithHeight,
Expand All @@ -144,21 +145,27 @@ const useManchettesWithSpaceTimeChart = (
[waypointWithHeight, zoomYIn, zoomYOut, resetZoom, toggleMode, yZoom, isProportional]
);

// Memoize spaceTimeChartProps separately
const handleXZoom = useCallback(
(newXZoom: number, xPosition = (spaceTimeChartRef?.current?.offsetWidth || 0) / 2) => {
setState((prev) => ({
...prev,
...zoomX(prev.xZoom, prev.xOffset, newXZoom, xPosition),
}));
},
[spaceTimeChartRef]
);

const spaceTimeChartProps = useMemo(
() => ({
operationalPoints: operationalPointsWithPosition,
spaceScales: computedScales,
timeScale: timeWindow / xZoom,
timeScale: zoomValueToTimeScale(xZoom),
paths,
xOffset,
yOffset: -scrollPosition + 14,
onZoom: (payload: Parameters<NonNullable<SpaceTimeChartProps['onZoom']>>[0]) => {
onZoom: ({ delta, position }: Parameters<NonNullable<SpaceTimeChartProps['onZoom']>>[0]) => {
if (isShiftPressed) {
setState((prev) => ({
...prev,
...zoomX(prev.xZoom, prev.xOffset, payload),
}));
handleXZoom(xZoom + delta, position.x);
}
},
onPan: (payload: {
Expand Down Expand Up @@ -195,7 +202,6 @@ const useManchettesWithSpaceTimeChart = (
[
operationalPointsWithPosition,
computedScales,
timeWindow,
xZoom,
paths,
xOffset,
Expand All @@ -205,6 +211,7 @@ const useManchettesWithSpaceTimeChart = (
panning,
yOffset,
manchetteWithSpaceTimeChartContainer,
handleXZoom,
]
);

Expand All @@ -213,8 +220,10 @@ const useManchettesWithSpaceTimeChart = (
manchetteProps,
spaceTimeChartProps,
handleScroll,
handleXZoom,
xZoom,
}),
[manchetteProps, spaceTimeChartProps, handleScroll]
[manchetteProps, spaceTimeChartProps, handleScroll, handleXZoom, xZoom]
);
};

Expand Down
Loading
Loading