|
| 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