Skip to content

Commit

Permalink
front: simulationresults: add a download button to get a trainschedul…
Browse files Browse the repository at this point in the history
…e csv
  • Loading branch information
nicolaswurtz committed Mar 1, 2024
1 parent 218b6fb commit 39f807b
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -40,9 +42,9 @@ export default function DriverTrainScheduleHeader({
return (
<>
<div className="d-flex align-items-center">
<h1 className="text-blue mt-2">{train.name}</h1>
<h1 className="text-blue flex-grow-1">{train.name}</h1>
{train.eco?.stops && (
<div className="ml-auto text-uppercase">
<div className="text-uppercase">
<OptionsSNCF
name="driver-train-schedule-base-or-eco"
sm
Expand All @@ -52,6 +54,14 @@ export default function DriverTrainScheduleHeader({
/>
</div>
)}
<button
type="button"
className="btn btn-link ml-2"
onClick={() => exportTrainCSV(train, baseOrEco)}
aria-label="train-csv"
>
<GoDownload size="24" />
</button>
</div>
<div className="row no-gutters align-items-center">
<div className="col-hd-3 col-xl-4 col-lg-6 small">
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}

0 comments on commit 39f807b

Please sign in to comment.