Skip to content

Commit

Permalink
manchette: fit horizontal scale at first render + add time zoom on sc…
Browse files Browse the repository at this point in the history
…roll+shift
  • Loading branch information
clarani committed Aug 29, 2024
1 parent c7c6cd2 commit 607adcb
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 13 deletions.
45 changes: 41 additions & 4 deletions ui-manchette/src/components/Manchette.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { type FC, useCallback, useEffect, useRef, useState } from 'react';
import React, { type FC, useCallback, useEffect, useRef, useState, useMemo } from 'react';

import { ZoomIn, ZoomOut } from '@osrd-project/ui-icons';
import { SpaceTimeChart, PathLayer } from '@osrd-project/ui-spacetimechart';
Expand All @@ -14,9 +14,11 @@ import {
} from './consts';
import {
calcOperationalPointsToDisplay,
computeTimeWindow,
getOperationalPointsWithPosition,
getScales,
operationalPointsHeight,
zoomX,
} from './helpers';
import OperationalPointList from './OperationalPointList';
import { useIsOverflow } from '../hooks/useIsOverFlow';
Expand Down Expand Up @@ -45,6 +47,8 @@ const Manchette: FC<ManchetteProps> = ({
}) => {
const manchette = useRef<HTMLDivElement>(null);

const [isShiftPressed, setIsShiftPressed] = useState(false);

const [state, setState] = useState<{
xZoom: number;
yZoom: number;
Expand Down Expand Up @@ -86,23 +90,44 @@ const Manchette: FC<ManchetteProps> = ({

const paths = usePaths(projectPathTrainResult, selectedProjection);

const timeWindow = useMemo(
() => computeTimeWindow(projectPathTrainResult),
[projectPathTrainResult]
);

const zoomYIn = useCallback(() => {
if (yZoom < MAX_ZOOM_Y) setState((prev) => ({ ...prev, yZoom: yZoom + ZOOM_Y_DELTA }));
}, [yZoom]);

const zoomYOut = useCallback(() => {
if (yZoom > MIN_ZOOM_Y) setState((prev) => ({ ...prev, yZoom: yZoom - ZOOM_Y_DELTA }));
}, [yZoom]);

const handleScroll = useCallback(() => {
if (manchette.current) {
if (!isShiftPressed && manchette.current) {
const { scrollTop } = manchette.current;
if (scrollTop || scrollTop === 0) {
setState((prev) => ({ ...prev, scrollPosition: scrollTop, yOffset: scrollTop }));
}
}
}, [isShiftPressed]);

const handleKeyDown = useCallback((event: KeyboardEvent) => {
if (event.key === 'Shift') {
setIsShiftPressed(true);
}
}, []);

const handleKeyUp = useCallback((event: KeyboardEvent) => {
if (event.key === 'Shift') {
setIsShiftPressed(false);
}
}, []);

const toggleMode = useCallback(() => {
setState((prev) => ({ ...prev, isProportional: !prev.isProportional }));
}, []);

const checkOverflow = useCallback((isOverflowFromCallback: boolean) => {
setState((prev) => ({ ...prev, panY: isOverflowFromCallback }));
}, []);
Expand All @@ -111,11 +136,15 @@ const Manchette: FC<ManchetteProps> = ({

useEffect(() => {
window.addEventListener('scroll', handleScroll, { passive: true });
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);

return () => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, [handleScroll]);
}, [handleScroll, handleKeyDown, handleKeyUp]);

useEffect(() => {
const computedOperationalPoints = calcOperationalPointsToDisplay(
Expand Down Expand Up @@ -208,7 +237,7 @@ const Manchette: FC<ManchetteProps> = ({
timeOrigin={Math.min(
...projectPathTrainResult.map((p) => +new Date(p.departure_time))
)}
timeScale={60000 / xZoom}
timeScale={timeWindow / xZoom}
xOffset={xOffset}
yOffset={-scrollPosition + 14}
onPan={({ initialPosition, position, isPanning }) => {
Expand Down Expand Up @@ -241,6 +270,14 @@ const Manchette: FC<ManchetteProps> = ({

setState(newState);
}}
onZoom={(payload) => {
if (isShiftPressed) {
setState((prev) => ({
...prev,
...zoomX(state.xZoom, state.xOffset, payload),
}));
}
}}
>
{paths.map((path, i) => (
<PathLayer key={path.id} index={i} path={path} color={path.color} />
Expand Down
2 changes: 1 addition & 1 deletion ui-manchette/src/components/OperationalPoint.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';

import { positionMmToKm } from './helpers';
import { type StyledOperationalPointType } from '../types';
import '@osrd-project/ui-core/dist/theme.css';
import { positionMmToKm } from '../utils';

const OperationalPoint: React.FC<StyledOperationalPointType> = ({
extensions,
Expand Down
3 changes: 2 additions & 1 deletion ui-manchette/src/components/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const INITIAL_OP_LIST_HEIGHT = 521;
export const INITIAL_SPACE_TIME_CHART_HEIGHT = INITIAL_OP_LIST_HEIGHT + 40;

export const MIN_ZOOM_X = 1;
export const MAX_ZOOM_X = 1;
export const MAX_ZOOM_X = 10;
export const ZOOM_X_DELTA = 0.5;

export const MIN_ZOOM_Y = 1;
Expand All @@ -12,3 +12,4 @@ export const ZOOM_Y_DELTA = 0.5;

export const FOOTER_HEIGHT = 40; // height of the manchette footer
export const OP_LINE_HEIGHT = 16;
export const MAX_TIME_WINDOW = 60 * 60 * 12; // 12 hours
62 changes: 55 additions & 7 deletions ui-manchette/src/components/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import type { OperationalPoint } from '@osrd-project/ui-spacetimechart/dist/lib/types';
import type {
OperationalPoint,
SpaceTimeChartProps,
} from '@osrd-project/ui-spacetimechart/dist/lib/types';

import { BASE_OP_HEIGHT, FOOTER_HEIGHT, OP_LINE_HEIGHT } from './consts';
import type { OperationalPointType, StyledOperationalPointType } from '../types';

const getHeightWithoutFooter = (height: number) => height - FOOTER_HEIGHT;

export const positionMmToKm = (position: number) => Math.round((position / 1000000) * 10) / 10;
import {
BASE_OP_HEIGHT,
FOOTER_HEIGHT,
OP_LINE_HEIGHT,
MAX_TIME_WINDOW,
MAX_ZOOM_X,
MIN_ZOOM_X,
} from './consts';
import type {
OperationalPointType,
ProjectPathTrainResult,
StyledOperationalPointType,
} from '../types';
import { getHeightWithoutFooter, msToS } from '../utils';

export const calcOperationalPointsToDisplay = (
operationalPoints: OperationalPointType[],
Expand Down Expand Up @@ -90,6 +101,28 @@ export const operationalPointsHeight = (
});
};

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

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

const firstPoint = Number(new Date(train.departure_time));
const lastPoint = Number(new Date(train.departure_time)) + 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 getOperationalPointsWithPosition = (
operationalPoints: StyledOperationalPointType[]
): OperationalPoint[] =>
Expand Down Expand Up @@ -125,3 +158,18 @@ export const getScales = (
},
];
};

/** 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]
) => {
const xZoom = Math.min(Math.max(currentXZoom * (1 + delta / 10), MIN_ZOOM_X), MAX_ZOOM_X);
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,
};
};
7 changes: 7 additions & 0 deletions ui-manchette/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { FOOTER_HEIGHT } from '../components/consts';

export const getHeightWithoutFooter = (height: number) => height - FOOTER_HEIGHT;

export const positionMmToKm = (position: number) => Math.round((position / 1000000) * 10) / 10;

export const msToS = (time: number) => time / 1000;

0 comments on commit 607adcb

Please sign in to comment.