diff --git a/front/src/modules/trainschedule/components/DriverTrainSchedule/DriverTrainScheduleHeader.tsx b/front/src/modules/trainschedule/components/DriverTrainSchedule/DriverTrainScheduleHeader.tsx index a9a1b3b28b6..74004093125 100644 --- a/front/src/modules/trainschedule/components/DriverTrainSchedule/DriverTrainScheduleHeader.tsx +++ b/front/src/modules/trainschedule/components/DriverTrainSchedule/DriverTrainScheduleHeader.tsx @@ -2,12 +2,14 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { jouleToKwh } from 'utils/physics'; -import { Train } from 'reducers/osrdsimulation/types'; -import { LightRollingStock } from 'common/api/osrdEditoastApi'; +import type { Train } from 'reducers/osrdsimulation/types'; +import type { LightRollingStock } from 'common/api/osrdEditoastApi'; import OptionsSNCF from 'common/BootstrapSNCF/OptionsSNCF'; import cx from 'classnames'; +import { GoDownload } from 'react-icons/go'; import { massWithOneDecimal } from './DriverTrainScheduleHelpers'; -import { BaseOrEco, BaseOrEcoType } from './DriverTrainScheduleTypes'; +import { BaseOrEco, type BaseOrEcoType } from './DriverTrainScheduleTypes'; +import exportTrainCSV from './driverTrainScheduleExportCSV'; type Props = { train: Train; @@ -40,9 +42,9 @@ export default function DriverTrainScheduleHeader({ return ( <>
-

{train.name}

+

{train.name}

{train.eco?.stops && ( -
+
)} +
diff --git a/front/src/modules/trainschedule/components/DriverTrainSchedule/driverTrainScheduleExportCSV.ts b/front/src/modules/trainschedule/components/DriverTrainSchedule/driverTrainScheduleExportCSV.ts new file mode 100644 index 00000000000..200d1cdd9f2 --- /dev/null +++ b/front/src/modules/trainschedule/components/DriverTrainSchedule/driverTrainScheduleExportCSV.ts @@ -0,0 +1,153 @@ +import type { + PositionSpeedTime, + Regime, + SpeedPosition, + Train, +} from 'reducers/osrdsimulation/types'; +import * as d3 from 'd3'; +import { BaseOrEcoType } from './DriverTrainScheduleTypes'; + +/** + * CSV Export of trainschedule + * + * Rows : position in km, speed in km/h, speed limit in km/h, time in s + * + */ + +enum CSVKeys { + op = 'op', + ch = 'ch', + lineCode = 'lineCode', + trackName = 'trackName', + position = 'position', + speed = 'speed', + speedLimit = 'speedLimit', + time = 'time', +} + +type CSVData = { + [key in keyof typeof CSVKeys]: string; +}; + +type PositionSpeedTimeOP = PositionSpeedTime & { + speedLimit?: number; + op?: ''; + ch?: ''; + lineCode?: ''; + trackName?: ''; +}; + +const pointToComma = (number: number) => number.toString().replace('.', ','); + +const interpolateValue = ( + position: number, + speeds: PositionSpeedTime[], + value: 'speed' | 'time' +): number => { + const bisector = d3.bisectLeft( + speeds.map((d: SpeedPosition) => d.position), + position + ); + + if (bisector === 0) return speeds[bisector][value]; + + const leftSpeed = speeds[bisector - 1]; + const rightSpeed = speeds[bisector]; + + const totalDistance = rightSpeed.position - leftSpeed.position; + const distance = position - leftSpeed.position; + const totalDifference = rightSpeed[value] - leftSpeed[value]; + return leftSpeed[value] + (totalDifference * distance) / totalDistance; +}; + +const getStepSpeedLimit = (position: number, speedLimitList: Train['vmax']) => { + const bisector = d3.bisectLeft( + speedLimitList.map((d: SpeedPosition) => d.position), + position + ); + return speedLimitList[bisector].speed || 0; +}; + +// Add OPs inside speedsteps array, gather speedlimit with stop position, and sort the array along position before return +const overloadWithOPsAndSpeedLimits = ( + trainRegime: Regime, + speedLimits: SpeedPosition[] +): PositionSpeedTimeOP[] => { + const speedsAtOps = trainRegime.stops.map((stop) => ({ + position: stop.position, + speed: interpolateValue(stop.position, trainRegime.speeds, 'speed'), + time: stop.time, + op: stop.name, + ch: stop.ch, + lineCode: stop.line_code, + trackName: stop.track_name, + })); + const speedsAtSpeedLimitChange = speedLimits.map((speedLimit) => ({ + position: speedLimit.position, + speed: interpolateValue(speedLimit.position, trainRegime.speeds, 'speed'), + speedLimit: speedLimit.speed, + time: interpolateValue(speedLimit.position, trainRegime.speeds, 'time'), + })); + + const speedsWithOPsAndSpeedLimits = trainRegime.speeds.concat( + speedsAtOps, + speedsAtSpeedLimitChange + ); + + return speedsWithOPsAndSpeedLimits.sort((stepA, stepB) => stepA.position - stepB.position); +}; + +function spreadTrackAndLineNames(steps: CSVData[]): CSVData[] { + let oldTrackName = ''; + let oldLineCode = ''; + const newSteps: CSVData[] = []; + steps.forEach((step) => { + const newTrackName = + oldTrackName !== '' && step.trackName === '' ? oldTrackName : step.trackName; + const newLineCode = oldLineCode !== '' && step.lineCode === '' ? oldLineCode : step.lineCode; + newSteps.push({ + ...step, + trackName: newTrackName, + lineCode: newLineCode, + }); + oldTrackName = newTrackName; + oldLineCode = newLineCode; + }); + return newSteps; +} + +function createFakeLinkWithData(train: Train, baseOrEco: BaseOrEcoType, csvData: CSVData[]) { + const currentDate = new Date(); + const header = `Date: ${currentDate.toLocaleString()}\nName: ${train.name}\nType:${baseOrEco}\n`; + const keyLine = `${Object.values(CSVKeys).join(';')}\n`; + const csvContent = `data:text/csv;charset=utf-8,${header}\n${keyLine}${csvData + .map((obj) => Object.values(obj).join(';')) + .join('\n')}`; + const encodedUri = encodeURI(csvContent); + const fakeLink = document.createElement('a'); + fakeLink.setAttribute('href', encodedUri); + fakeLink.setAttribute('download', `export-${train.name}-${baseOrEco}.csv`); + document.body.appendChild(fakeLink); + fakeLink.click(); + document.body.removeChild(fakeLink); +} + +export default function driverTrainScheduleExportCSV(train: Train, baseOrEco: BaseOrEcoType) { + const trainRegime = train[baseOrEco]; + if (trainRegime) { + const speedsWithOPsAndSpeedLimits = overloadWithOPsAndSpeedLimits(trainRegime, train.vmax); + const steps = speedsWithOPsAndSpeedLimits.map((speed) => ({ + op: speed.op || '', + ch: speed.ch || '', + lineCode: speed.lineCode || '', + trackName: speed.trackName || '', + position: pointToComma(speed.position / 1000), + speed: pointToComma(speed.speed * 3.6), + speedLimit: pointToComma( + Math.round((speed.speedLimit ?? getStepSpeedLimit(speed.position, train.vmax)) * 3.6) + ), + time: pointToComma(speed.time), + })); + if (steps) createFakeLinkWithData(train, baseOrEco, spreadTrackAndLineNames(steps)); + } +}