-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ui-spacetimechart: work schedules on the space time chart
Signed-off-by: Valentin Chanas <anisometropie@gmail.com>
- Loading branch information
1 parent
9949bbf
commit 7022497
Showing
7 changed files
with
259 additions
and
0 deletions.
There are no files selected for viewing
7 changes: 7 additions & 0 deletions
7
ui-spacetimechart/src/assets/images/ScheduledMaintenanceUp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { useCallback } from 'react'; | ||
|
||
import { useDraw } from '../hooks/useCanvas'; | ||
import { type DrawingFunction } from '../lib/types'; | ||
|
||
export type PatternRectProps = { | ||
timeStart: Date; | ||
timeEnd: Date; | ||
spaceStart: number; // mm | ||
spaceEnd: number; // mm | ||
imageElement: HTMLImageElement; | ||
}; | ||
|
||
/** | ||
* draws a repeating pattern in the space time chart | ||
*/ | ||
export const PatternRect = ({ | ||
timeStart, | ||
timeEnd, | ||
spaceStart, | ||
spaceEnd, | ||
imageElement, | ||
}: PatternRectProps) => { | ||
const drawRegion = useCallback<DrawingFunction>( | ||
(ctx, { getSpacePixel, getTimePixel, spaceAxis }) => { | ||
const timeStartPixel = getTimePixel(Number(timeStart)); | ||
const endTimePixel = getTimePixel(Number(timeEnd)); | ||
const spaceStartPixel = getSpacePixel(spaceStart); | ||
const spaceEndPixel = getSpacePixel(spaceEnd); | ||
|
||
const areaSpaceSize = spaceEndPixel - spaceStartPixel; | ||
const areaTimeSize = endTimePixel - timeStartPixel; | ||
if (!areaSpaceSize || !areaTimeSize) return; | ||
|
||
const pattern = ctx.createPattern(imageElement, 'repeat'); | ||
if (!pattern) { | ||
return; | ||
} | ||
|
||
ctx.save(); | ||
ctx.fillStyle = pattern; | ||
if (spaceAxis === 'x') { | ||
ctx.translate(spaceStartPixel, timeStartPixel); | ||
ctx.fillRect(0, 0, areaSpaceSize, areaTimeSize); | ||
} else { | ||
ctx.translate(timeStartPixel, spaceStartPixel); | ||
ctx.fillRect(0, 0, areaTimeSize, areaSpaceSize); | ||
} | ||
ctx.restore(); | ||
}, | ||
[timeStart, timeEnd, spaceStart, spaceEnd, imageElement] | ||
); | ||
useDraw('background', drawRegion); | ||
|
||
return null; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import React, { useEffect, useState } from 'react'; | ||
|
||
import { PatternRect } from './PatternRect'; | ||
import { type WorkSchedule } from '../types'; | ||
|
||
type WorkScheduleLayerProps = { | ||
workSchedules: WorkSchedule[]; | ||
imageUrl: string; | ||
}; | ||
|
||
/** | ||
* Displays the workschedule projection on the Space time chart. | ||
* A workschedule has a start time and an end time. | ||
* It can contain several portions of a track occupied during this period. | ||
* Each portion is represented by a rectangle region | ||
*/ | ||
export const WorkScheduleLayer = ({ workSchedules, imageUrl }: WorkScheduleLayerProps) => { | ||
const [imageElement, setImageElement] = useState<HTMLImageElement>(); | ||
useEffect(() => { | ||
if (imageUrl) { | ||
const newImage = new Image(); | ||
newImage.src = imageUrl; | ||
newImage.onload = () => { | ||
setImageElement(newImage); | ||
}; | ||
} | ||
}, [imageUrl]); | ||
|
||
if (!imageElement) { | ||
return null; | ||
} | ||
|
||
return workSchedules.flatMap((ws) => | ||
ws.spaceRanges.map(([spaceStart, spaceEnd]) => ( | ||
<PatternRect | ||
key={`${ws.type}-${ws.timeStart}-${ws.timeEnd}-${spaceStart}-${spaceEnd}`} | ||
timeStart={ws.timeStart} | ||
timeEnd={ws.timeEnd} | ||
spaceStart={spaceStart} | ||
spaceEnd={spaceEnd} | ||
imageElement={imageElement} | ||
/> | ||
)) | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
declare module '*.svg' { | ||
const content: string; | ||
export default content; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
139 changes: 139 additions & 0 deletions
139
ui-spacetimechart/src/stories/work-schedules.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import React, { useState } from 'react'; | ||
|
||
import '@osrd-project/ui-core/dist/theme.css'; | ||
|
||
import type { Meta } from '@storybook/react'; | ||
|
||
import { OPERATIONAL_POINTS, PATHS } from './lib/paths'; | ||
import upward from '../assets/images/ScheduledMaintenanceUp.svg'; | ||
import { PathLayer } from '../components/PathLayer'; | ||
import { SpaceTimeChart } from '../components/SpaceTimeChart'; | ||
import { WorkScheduleLayer } from '../components/WorkScheduleLayer'; | ||
import { type Point, type PathData, type OperationalPoint } from '../lib/types'; | ||
import { type WorkSchedule } from '../types'; | ||
import { getDiff } from '../utils/vectors'; | ||
|
||
const SAMPLE_WORK_SCHEDULES: WorkSchedule[] = [ | ||
{ | ||
type: 'TRACK', | ||
timeStart: new Date('2024-04-02T00:00:00Z'), | ||
timeEnd: new Date('2024-04-02T00:15:00Z'), | ||
spaceRanges: [ | ||
[20000, 35000], | ||
[45000, 60000], | ||
], | ||
}, | ||
{ | ||
type: 'TRACK', | ||
timeStart: new Date('2024-04-02T00:15:00Z'), | ||
timeEnd: new Date('2024-04-02T01:00:00Z'), | ||
spaceRanges: [ | ||
[80000, 100000], | ||
[110000, 140000], | ||
], | ||
}, | ||
{ | ||
type: 'TRACK', | ||
timeStart: new Date('2024-04-02T01:30:00Z'), | ||
timeEnd: new Date('2024-04-02T02:30:00Z'), | ||
spaceRanges: [[50000, 100000]], | ||
}, | ||
]; | ||
|
||
const DEFAULT_HEIGHT = 550; | ||
|
||
type WorkSchedulesWrapperProps = { | ||
operationalPoints: OperationalPoint[]; | ||
paths: (PathData & { color: string })[]; | ||
workSchedules: WorkSchedule[]; | ||
}; | ||
|
||
const WorkSchedulesWrapper = ({ | ||
operationalPoints = [], | ||
paths = [], | ||
workSchedules, | ||
}: WorkSchedulesWrapperProps) => { | ||
const [state, setState] = useState<{ | ||
xOffset: number; | ||
yOffset: number; | ||
panning: null | { initialOffset: Point }; | ||
}>({ | ||
xOffset: -300, | ||
yOffset: 0, | ||
panning: null, | ||
}); | ||
const simpleOperationalPoints = operationalPoints.map(({ id, position }) => ({ | ||
id, | ||
label: id, | ||
position, | ||
})); | ||
const spaceScale = [ | ||
{ | ||
from: 0, | ||
to: 75000, | ||
coefficient: 300, | ||
}, | ||
]; | ||
return ( | ||
<div className="manchette-space-time-chart-wrapper" style={{ height: `${DEFAULT_HEIGHT}px` }}> | ||
<SpaceTimeChart | ||
className="inset-0 absolute h-full" | ||
spaceOrigin={0} | ||
xOffset={state.xOffset} | ||
yOffset={state.yOffset} | ||
timeOrigin={+new Date('2024/04/02')} | ||
operationalPoints={simpleOperationalPoints} | ||
timeScale={10000} | ||
spaceScales={spaceScale} | ||
onPan={({ initialPosition, position, isPanning }) => { | ||
const diff = getDiff(initialPosition, position); | ||
setState((s) => { | ||
// Stop panning: | ||
if (!isPanning) { | ||
return { ...s, panning: null }; | ||
} | ||
// Start panning: | ||
else if (!s.panning) { | ||
return { | ||
...s, | ||
panning: { | ||
initialOffset: { | ||
x: s.xOffset, | ||
y: s.yOffset, | ||
}, | ||
}, | ||
}; | ||
} | ||
// Keep panning: | ||
else { | ||
const { initialOffset } = s.panning; | ||
return { | ||
...s, | ||
xOffset: initialOffset.x + diff.x, | ||
yOffset: initialOffset.y + diff.y, | ||
}; | ||
} | ||
}); | ||
}} | ||
> | ||
{paths.map((path) => ( | ||
<PathLayer key={path.id} path={path} color={path.color} /> | ||
))} | ||
<WorkScheduleLayer workSchedules={workSchedules} imageUrl={upward} /> | ||
</SpaceTimeChart> | ||
</div> | ||
); | ||
}; | ||
|
||
export default { | ||
title: 'SpaceTimeChart/Workschedules', | ||
component: WorkSchedulesWrapper, | ||
} as Meta<typeof WorkSchedulesWrapper>; | ||
|
||
export const Default = { | ||
args: { | ||
operationalPoints: OPERATIONAL_POINTS, | ||
paths: PATHS.slice(2, 4), | ||
workSchedules: SAMPLE_WORK_SCHEDULES, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export type WorkSchedule = { | ||
type: 'TRACK' | 'CATENARY'; | ||
timeStart: Date; | ||
timeEnd: Date; | ||
spaceRanges: [number, number][]; | ||
}; |