Skip to content

Commit 39f807b

Browse files
committed
front: simulationresults: add a download button to get a trainschedule csv
1 parent 218b6fb commit 39f807b

File tree

2 files changed

+168
-5
lines changed

2 files changed

+168
-5
lines changed

front/src/modules/trainschedule/components/DriverTrainSchedule/DriverTrainScheduleHeader.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import React from 'react';
22
import { useTranslation } from 'react-i18next';
33

44
import { jouleToKwh } from 'utils/physics';
5-
import { Train } from 'reducers/osrdsimulation/types';
6-
import { LightRollingStock } from 'common/api/osrdEditoastApi';
5+
import type { Train } from 'reducers/osrdsimulation/types';
6+
import type { LightRollingStock } from 'common/api/osrdEditoastApi';
77
import OptionsSNCF from 'common/BootstrapSNCF/OptionsSNCF';
88
import cx from 'classnames';
9+
import { GoDownload } from 'react-icons/go';
910
import { massWithOneDecimal } from './DriverTrainScheduleHelpers';
10-
import { BaseOrEco, BaseOrEcoType } from './DriverTrainScheduleTypes';
11+
import { BaseOrEco, type BaseOrEcoType } from './DriverTrainScheduleTypes';
12+
import exportTrainCSV from './driverTrainScheduleExportCSV';
1113

1214
type Props = {
1315
train: Train;
@@ -40,9 +42,9 @@ export default function DriverTrainScheduleHeader({
4042
return (
4143
<>
4244
<div className="d-flex align-items-center">
43-
<h1 className="text-blue mt-2">{train.name}</h1>
45+
<h1 className="text-blue flex-grow-1">{train.name}</h1>
4446
{train.eco?.stops && (
45-
<div className="ml-auto text-uppercase">
47+
<div className="text-uppercase">
4648
<OptionsSNCF
4749
name="driver-train-schedule-base-or-eco"
4850
sm
@@ -52,6 +54,14 @@ export default function DriverTrainScheduleHeader({
5254
/>
5355
</div>
5456
)}
57+
<button
58+
type="button"
59+
className="btn btn-link ml-2"
60+
onClick={() => exportTrainCSV(train, baseOrEco)}
61+
aria-label="train-csv"
62+
>
63+
<GoDownload size="24" />
64+
</button>
5565
</div>
5666
<div className="row no-gutters align-items-center">
5767
<div className="col-hd-3 col-xl-4 col-lg-6 small">
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import type {
2+
PositionSpeedTime,
3+
Regime,
4+
SpeedPosition,
5+
Train,
6+
} from 'reducers/osrdsimulation/types';
7+
import * as d3 from 'd3';
8+
import { BaseOrEcoType } from './DriverTrainScheduleTypes';
9+
10+
/**
11+
* CSV Export of trainschedule
12+
*
13+
* Rows : position in km, speed in km/h, speed limit in km/h, time in s
14+
*
15+
*/
16+
17+
enum CSVKeys {
18+
op = 'op',
19+
ch = 'ch',
20+
lineCode = 'lineCode',
21+
trackName = 'trackName',
22+
position = 'position',
23+
speed = 'speed',
24+
speedLimit = 'speedLimit',
25+
time = 'time',
26+
}
27+
28+
type CSVData = {
29+
[key in keyof typeof CSVKeys]: string;
30+
};
31+
32+
type PositionSpeedTimeOP = PositionSpeedTime & {
33+
speedLimit?: number;
34+
op?: '';
35+
ch?: '';
36+
lineCode?: '';
37+
trackName?: '';
38+
};
39+
40+
const pointToComma = (number: number) => number.toString().replace('.', ',');
41+
42+
const interpolateValue = (
43+
position: number,
44+
speeds: PositionSpeedTime[],
45+
value: 'speed' | 'time'
46+
): number => {
47+
const bisector = d3.bisectLeft(
48+
speeds.map((d: SpeedPosition) => d.position),
49+
position
50+
);
51+
52+
if (bisector === 0) return speeds[bisector][value];
53+
54+
const leftSpeed = speeds[bisector - 1];
55+
const rightSpeed = speeds[bisector];
56+
57+
const totalDistance = rightSpeed.position - leftSpeed.position;
58+
const distance = position - leftSpeed.position;
59+
const totalDifference = rightSpeed[value] - leftSpeed[value];
60+
return leftSpeed[value] + (totalDifference * distance) / totalDistance;
61+
};
62+
63+
const getStepSpeedLimit = (position: number, speedLimitList: Train['vmax']) => {
64+
const bisector = d3.bisectLeft(
65+
speedLimitList.map((d: SpeedPosition) => d.position),
66+
position
67+
);
68+
return speedLimitList[bisector].speed || 0;
69+
};
70+
71+
// Add OPs inside speedsteps array, gather speedlimit with stop position, and sort the array along position before return
72+
const overloadWithOPsAndSpeedLimits = (
73+
trainRegime: Regime,
74+
speedLimits: SpeedPosition[]
75+
): PositionSpeedTimeOP[] => {
76+
const speedsAtOps = trainRegime.stops.map((stop) => ({
77+
position: stop.position,
78+
speed: interpolateValue(stop.position, trainRegime.speeds, 'speed'),
79+
time: stop.time,
80+
op: stop.name,
81+
ch: stop.ch,
82+
lineCode: stop.line_code,
83+
trackName: stop.track_name,
84+
}));
85+
const speedsAtSpeedLimitChange = speedLimits.map((speedLimit) => ({
86+
position: speedLimit.position,
87+
speed: interpolateValue(speedLimit.position, trainRegime.speeds, 'speed'),
88+
speedLimit: speedLimit.speed,
89+
time: interpolateValue(speedLimit.position, trainRegime.speeds, 'time'),
90+
}));
91+
92+
const speedsWithOPsAndSpeedLimits = trainRegime.speeds.concat(
93+
speedsAtOps,
94+
speedsAtSpeedLimitChange
95+
);
96+
97+
return speedsWithOPsAndSpeedLimits.sort((stepA, stepB) => stepA.position - stepB.position);
98+
};
99+
100+
function spreadTrackAndLineNames(steps: CSVData[]): CSVData[] {
101+
let oldTrackName = '';
102+
let oldLineCode = '';
103+
const newSteps: CSVData[] = [];
104+
steps.forEach((step) => {
105+
const newTrackName =
106+
oldTrackName !== '' && step.trackName === '' ? oldTrackName : step.trackName;
107+
const newLineCode = oldLineCode !== '' && step.lineCode === '' ? oldLineCode : step.lineCode;
108+
newSteps.push({
109+
...step,
110+
trackName: newTrackName,
111+
lineCode: newLineCode,
112+
});
113+
oldTrackName = newTrackName;
114+
oldLineCode = newLineCode;
115+
});
116+
return newSteps;
117+
}
118+
119+
function createFakeLinkWithData(train: Train, baseOrEco: BaseOrEcoType, csvData: CSVData[]) {
120+
const currentDate = new Date();
121+
const header = `Date: ${currentDate.toLocaleString()}\nName: ${train.name}\nType:${baseOrEco}\n`;
122+
const keyLine = `${Object.values(CSVKeys).join(';')}\n`;
123+
const csvContent = `data:text/csv;charset=utf-8,${header}\n${keyLine}${csvData
124+
.map((obj) => Object.values(obj).join(';'))
125+
.join('\n')}`;
126+
const encodedUri = encodeURI(csvContent);
127+
const fakeLink = document.createElement('a');
128+
fakeLink.setAttribute('href', encodedUri);
129+
fakeLink.setAttribute('download', `export-${train.name}-${baseOrEco}.csv`);
130+
document.body.appendChild(fakeLink);
131+
fakeLink.click();
132+
document.body.removeChild(fakeLink);
133+
}
134+
135+
export default function driverTrainScheduleExportCSV(train: Train, baseOrEco: BaseOrEcoType) {
136+
const trainRegime = train[baseOrEco];
137+
if (trainRegime) {
138+
const speedsWithOPsAndSpeedLimits = overloadWithOPsAndSpeedLimits(trainRegime, train.vmax);
139+
const steps = speedsWithOPsAndSpeedLimits.map((speed) => ({
140+
op: speed.op || '',
141+
ch: speed.ch || '',
142+
lineCode: speed.lineCode || '',
143+
trackName: speed.trackName || '',
144+
position: pointToComma(speed.position / 1000),
145+
speed: pointToComma(speed.speed * 3.6),
146+
speedLimit: pointToComma(
147+
Math.round((speed.speedLimit ?? getStepSpeedLimit(speed.position, train.vmax)) * 3.6)
148+
),
149+
time: pointToComma(speed.time),
150+
}));
151+
if (steps) createFakeLinkWithData(train, baseOrEco, spreadTrackAndLineNames(steps));
152+
}
153+
}

0 commit comments

Comments
 (0)