import React, { useEffect, useMemo, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { KebabHorizontal } from '../../../ui-icons/src/index'; import TimeCaptions from '../../../ui-spacetimechart/src/components/TimeCaptions'; import { useCanvas, useDraw } from '../../../ui-spacetimechart/src/hooks/useCanvas'; import { useMouseInteractions } from '../../../ui-spacetimechart/src/hooks/useMouseInteractions'; import { useMouseTracking } from '../../../ui-spacetimechart/src/hooks/useMouseTracking'; import { useSize } from '../../../ui-spacetimechart/src/hooks/useSize'; import { DEFAULT_THEME } from '../../../ui-spacetimechart/src/lib/consts'; import { CanvasContext, MouseContext, SpaceTimeChartContext, } from '../../../ui-spacetimechart/src/lib/context'; import type { MouseContextType, SpaceTimeChartContextType, PickingElement, SpaceTimeChartTheme, } from '../../../ui-spacetimechart/src/lib/types'; import { OPERATIONAL_POINTS } from '../../../ui-spacetimechart/src/stories/lib/paths'; import { getTimeToPixel, getSpaceToPixel, getDataToPoint, getPixelToTime, getPixelToSpace, getPointToData, spaceScalesToBinaryTree, } from '../../../ui-spacetimechart/src/utils/scales'; import { TrackOccupancyManchette, TrackOccupancyCanvas, } from '../../../ui-trackoccupancydiagram/src/index'; import occupancyZones from '../samples/TrackOccupancyDiagramSamples/occupancyZones'; import tracks from '../samples/TrackOccupancyDiagramSamples/tracks'; type TrackOccupancyDiagramProps = { xZoomLevel: number; yZoomLevel: number; xOffset: number; yOffset: number; spaceScaleType: 'linear' | 'proportional'; emptyData: boolean; selectedTrainId: string; setSelectedTrainId: (id: string) => void; }; const OP_ID = 'story'; const X_ZOOM_LEVEL = 6; const Y_ZOOM_LEVEL = 3; const SELECTED_TRAIN_ID = '5'; const TrackOccupancyDiagram = ({ xZoomLevel, yZoomLevel, xOffset, yOffset, spaceScaleType, emptyData, selectedTrainId, setSelectedTrainId, }: TrackOccupancyDiagramProps) => { const spaceOrigin = 0; const [root, setRoot] = useState<HTMLDivElement | null>(null); const { width, height } = useSize(root); const [canvasesRoot, setCanvasesRoot] = useState<HTMLDivElement | null>(null); const { width: trackOccupancyWidth, height: trackOccupancyHeight } = useSize(canvasesRoot); const timeOrigin = +new Date('2024/04/02'); const timeScale = 60000 / xZoomLevel; const swapAxis = undefined; const hideGrid = undefined; const hidePathsLabels = undefined; const enableSnapping = undefined; const showTicks = true; const fullTheme: SpaceTimeChartTheme = { ...DEFAULT_THEME, background: 'transparent', timeGraduationsStyles: { ...DEFAULT_THEME.timeGraduationsStyles, 1: { ...DEFAULT_THEME.timeGraduationsStyles[1], color: 'transparent' }, }, }; const operationalPoints = useMemo(() => (emptyData ? [] : OPERATIONAL_POINTS), [emptyData]); const spaceScales = useMemo(() => { if (emptyData) { return []; } return operationalPoints.slice(0, -1).map((point, i) => ({ from: point.position, to: operationalPoints[i + 1].position, ...(spaceScaleType === 'linear' ? { size: 50 * yZoomLevel } : { coefficient: 150 / yZoomLevel }), })); }, [emptyData, operationalPoints, spaceScaleType, yZoomLevel]); const fingerprint = useMemo( () => JSON.stringify({ width, height, spaceOrigin, spaceScales, timeOrigin, timeScale, xOffset, yOffset, swapAxis, hideGrid, hidePathsLabels, showTicks, }), [ width, height, spaceOrigin, spaceScales, timeOrigin, timeScale, xOffset, yOffset, swapAxis, hideGrid, hidePathsLabels, showTicks, ] ); // TODO: when occupancyZones layer and zoom/pan are implemented, clean all unneeded variables from contextState, variables declared before contextState, and props. If needed, create a new context type. const contextState: SpaceTimeChartContextType = useMemo(() => { const spaceScaleTree = spaceScalesToBinaryTree(spaceOrigin, spaceScales); const timeAxis = !swapAxis ? 'x' : 'y'; const spaceAxis = !swapAxis ? 'y' : 'x'; // Data translation helpers: let timePixelOffset; let spacePixelOffset; if (!swapAxis) { timePixelOffset = xOffset; spacePixelOffset = yOffset; } else { timePixelOffset = yOffset; spacePixelOffset = xOffset; } const getTimePixel = getTimeToPixel(timeOrigin, timePixelOffset, timeScale); const getSpacePixel = getSpaceToPixel(spacePixelOffset, spaceScaleTree); const getPoint = getDataToPoint(getTimePixel, getSpacePixel, timeAxis, spaceAxis); const getTime = getPixelToTime(timeOrigin, timePixelOffset, timeScale); const getSpace = getPixelToSpace(spaceOrigin, spacePixelOffset, spaceScaleTree); const getData = getPointToData(getTime, getSpace, timeAxis, spaceAxis); const pickingElements: PickingElement[] = []; const resetPickingElements = () => { pickingElements.length = 0; }; const registerPickingElement = (element: PickingElement) => { pickingElements.push(element); return pickingElements.length - 1; }; return { fingerprint, width, height, trackOccupancyHeight, trackOccupancyWidth, getTimePixel, getSpacePixel, getPoint, getTime, getSpace, getData, pickingElements, resetPickingElements, registerPickingElement, operationalPoints, tracks, occupancyZones, spaceOrigin, spaceScaleTree, timeOrigin, timeScale, timePixelOffset, spacePixelOffset, timeAxis, spaceAxis, swapAxis: !!swapAxis, enableSnapping: !!enableSnapping, hideGrid: !!hideGrid, hidePathsLabels: !!hidePathsLabels, showTicks: !!showTicks, theme: fullTheme, }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [fingerprint]); const [spaceTicksRoot, setSpaceTicksRoot] = useState<HTMLDivElement | null>(null); const mouseState = useMouseTracking(root); const { position } = mouseState; const { canvasContext } = useCanvas(canvasesRoot, contextState, position); const { canvasContext: spaceTicksContext } = useCanvas(spaceTicksRoot, contextState, position); const mouseContext = useMemo<MouseContextType>( () => ({ isHover: false, position: mouseState.position, hoveredItem: null, data: contextState.getData(mouseState.position), }), [mouseState.position, contextState] ); const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); const onClick = () => { setMousePosition(mouseContext.position); }; useMouseInteractions(canvasesRoot, mouseContext, { onClick }, contextState); return ( <div id="track-occupancy-diagram-base-story" className="bg-ambientB-10"> <SpaceTimeChartContext.Provider value={contextState}> <div className="main-container"> <div className="bg-ambientB-5 flex flex-col justify-center main-container-header"> <KebabHorizontal /> </div> <div className="flex"> <div className="main-container-manchette"> <TrackOccupancyManchette tracks={tracks} /> </div> <CanvasContext.Provider value={canvasContext}> <MouseContext.Provider value={mouseContext}> <div className="main-container-canvas"> <TrackOccupancyCanvas opId={OP_ID} useDraw={useDraw} setCanvasesRoot={setCanvasesRoot} selectedTrainId={selectedTrainId} setSelectedTrainId={setSelectedTrainId} mousePosition={mousePosition} /> </div> </MouseContext.Provider> </CanvasContext.Provider> </div> </div> <CanvasContext.Provider value={spaceTicksContext}> <div ref={setRoot} className="relative main-container-time-captions"> <div ref={setSpaceTicksRoot} className="absolute inset-0"> <TimeCaptions /> </div> </div> </CanvasContext.Provider> </SpaceTimeChartContext.Provider> </div> ); }; const TrackOccupancyDiagramStory = ({ trainId }: { trainId: number }) => { const [selectedTrainId, setSelectedTrainId] = useState('0'); useEffect(() => { setSelectedTrainId(`${trainId}`); }, [trainId]); return ( <TrackOccupancyDiagram xZoomLevel={X_ZOOM_LEVEL} yZoomLevel={Y_ZOOM_LEVEL} xOffset={0} yOffset={0} spaceScaleType="linear" emptyData={false} selectedTrainId={selectedTrainId} setSelectedTrainId={setSelectedTrainId} /> ); }; const meta: Meta<typeof TrackOccupancyDiagramStory> = { title: 'TrackOccupancyDiagram/Rendering', component: TrackOccupancyDiagramStory, decorators: [(Story) => <Story />], parameters: { backgrounds: { default: 'lightSand', values: [ { name: 'lightSand', value: 'rgba(247, 246, 238, var(--tw-bg-opacity, 1))', }, ], }, }, args: { trainId: +SELECTED_TRAIN_ID, }, render: (args) => <TrackOccupancyDiagramStory {...args} />, tags: ['autodocs'], }; export default meta; type Story = StoryObj<typeof TrackOccupancyDiagramStory>; export const TrackOccupancyDiagramStoryDefault: Story = { args: { trainId: 5, }, };