Skip to content

Commit

Permalink
front: improved driver train schedule (#2409)
Browse files Browse the repository at this point in the history
  • Loading branch information
Akctarus authored and alexandredamiron committed Jan 17, 2023
1 parent ca42ea7 commit e56e364
Show file tree
Hide file tree
Showing 6 changed files with 437 additions and 14 deletions.
23 changes: 23 additions & 0 deletions front/public/locales/fr/drivertrainschedule.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"actualspeed": "Actuelle",
"actualspeed-short": "Act.",
"averagespeed": "Moyenne",
"averagespeed-short": "Moy.",
"composition": "Composition",
"dcmdetails": "Détails du sillon",
"destination": "Destination",
"mass": "Masse",
"numberoflines": "Nombre de lignes",
"origin": "Origine",
"pk": "PK",
"place": "Etablissement",
"placeholderline": "",
"rollingstock": "Matériel roulant",
"speed": "Vitesse",
"speedLimitByTag": "Code de composition",
"speedlimit": "Limite",
"speedlimit-short": "Lim.",
"time": "Horaire",
"timetable": "Table horaire",
"trackname": "Voie"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import React from 'react';
import * as d3 from 'd3';
import nextId from 'react-id-generator';

function getTime(sec) {
const timeplus = new Date(sec * 1000);
const time = timeplus.toISOString().substr(11, 8);
if (time[6] >= 0 && time[6] < 2) {
if (time[6] === '1') {
if (time[7] <= 4) {
return time.slice(0, 5);
}
return `${time.slice(0, 5)}+`;
}
return time.slice(0, 5);
}
if (time[6] >= 1 && time[6] < 5) {
if (time[6] === '4') {
if (time[7] <= 4) {
return `${time.slice(0, 5)}+`;
}
if (time[7] > 4) {
timeplus.setMinutes(timeplus.getMinutes() + 1);
timeplus.setSeconds(0);
return timeplus.toISOString().substr(11, 8).slice(0, 5);
}
}
return `${time.slice(0, 5)}+`;
}
timeplus.setMinutes(timeplus.getMinutes() + 1);
timeplus.setSeconds(0);
return timeplus.toISOString().substr(11, 8).slice(0, 5);
}

function getActualVmax(givenPosition, vmax) {
const vmaxPosition = d3.bisectLeft(
vmax.map((d) => d.position),
givenPosition
);
return Math.round(vmax[vmaxPosition].speed * 3.6);
}

function getActualSpeedLeft(givenPosition, speed) {
const speedPosition = d3.bisectLeft(
speed.map((d) => d.position),
givenPosition
);
return speed[speedPosition].speed * 3.6;
}

function getActualPositionLeft(givenPosition, speed) {
const speedPosition = d3.bisectLeft(
speed.map((d) => d.position),
givenPosition
);
return speed[speedPosition].position / 1000;
}

function getActualSpeedRight(givenPosition, speed) {
const speedPosition =
d3.bisectLeft(
speed.map((d) => d.position),
givenPosition
) - 1;
return speedPosition <= 0 ? speed[0].speed * 3.6 : speed[speedPosition].speed * 3.6;
}

function getActualPositionRight(givenPosition, speed) {
const speedPosition =
d3.bisectLeft(
speed.map((d) => d.position),
givenPosition
) - 1;
return speedPosition <= 0 ? speed[0].position / 1000 : speed[speedPosition].position / 1000;
}

function getActualSpeed(givenPosition, speed) {
const speedA = getActualSpeedRight(givenPosition, speed);
const speedB = getActualSpeedLeft(givenPosition, speed);
const posA = getActualPositionRight(givenPosition, speed);
const posB = getActualPositionLeft(givenPosition, speed);
if (speedA === 0 || speedB === 0) return 0;
const a = (speedB - speedA) / (posB - posA);
const b = speedA - a * posA;
return Math.round(a * (givenPosition / 1000) + b);
}

function getAverageSpeed(posA, posB, speedList) {
let totalDistance = 0;
const speedsAndDistances = [];
let averageSpeed = 0;

// Filter concerned speed by posA & posB (all speed sections between the two positions)
const concernedSpeeds = speedList.filter((item) => item.position >= posA && item.position < posB);

// When concernedSpeeds is empty or < 2, take nearest plateau speed
if (concernedSpeeds.length === 1) return Math.round(concernedSpeeds[0].speed * 3.6 * 10) / 10;
if (concernedSpeeds.length === 0) {
const lastKnownSpeedPosition = d3.bisectLeft(
speedList.map((step) => step.position),
posB
);
return Math.round(speedList[lastKnownSpeedPosition].speed * 3.6 * 10) / 10;
}

// Get an array with speed along distance and set a sum of distance
concernedSpeeds.forEach((actualPosition, idx) => {
if (idx !== 0) {
const actualDistance = actualPosition.position - speedList[idx - 1].position;
speedsAndDistances.push({ speed: actualPosition.speed, distance: actualDistance });
totalDistance += actualDistance;
}
});

// Weight speed with distance ratio and sum it for average speed
speedsAndDistances.forEach((step) => {
averageSpeed += step.speed * (step.distance / totalDistance);
});

// Get average speed with decimal in km/h
return Math.round(averageSpeed * 3.6 * 10) / 10;
}

export default function formatStops(stop, idx, data) {
const actualSpeed = getActualSpeed(stop.position, data.base.speeds);
const averageSpeed = getAverageSpeed(
idx === 0 ? stop.position : data.base.stops[idx - 1].position,
stop.position,
data.base.speeds
);
const stopTime =
getTime(stop.time).at(-1) === '+' ? (
<span className="timeWithPlus">{getTime(stop.time).slice(0, -1)}</span>
) : (
getTime(stop.time)
);
const pk = Math.round(stop.position / 100) / 10;
return (
<tr key={nextId()} className={`${stop.duration > 0 ? 'drivertrainschedule-stop' : ''}`}>
<td className="text-center">
<small>{idx + 1}</small>
</td>
<td className="text-center">{getActualVmax(stop.position, data.vmax)}</td>
<td className="text-center">{actualSpeed !== 0 ? actualSpeed : null}</td>
<td className="text-center">{averageSpeed}</td>
<td className="d-flex justify-content-center">
<div className="drivertrainschedule-pk">{Number.isInteger(pk) ? `${pk}.0` : pk}</div>
</td>
<td>{stop.name || 'Unknown'}</td>
<td className="text-center drivertrainschedule-stop-time">
{stop.duration > 0 ? (
<>
<span>{stopTime}</span>
<span className="ml-2">{stop.duration > 0 && getTime(stop.time + stop.duration)}</span>
</>
) : (
stopTime
)}
</td>
<td>
<div
className="text-center"
data-toggle="tooltip"
data-placement="top"
title={`${stop.line_name}`}
>
<small>{stop.track_name}</small>
</div>
</td>
</tr>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import formatStops from 'applications/osrd/components/DriverTrainSchedule/DriverTrainScheduleHelpers';

function originStop(stop) {
return <div className="text-primary">{stop.name || 'Unknown'}</div>;
}

export default function DriverTrainScheduleModal(props) {
const { data, rollingStockSelected } = props;
const { t } = useTranslation(['drivertrainschedule']);
return (
<div className="container-drivertrainschedule">
<h1 className="text-blue mt-2">
{t('drivertrainschedule:dcmdetails')}
<span className="ml-1 text-normal">{data.name}</span>
</h1>
<div className="row">
<div className="col-xl-4">
<div className="row no-gutters">
<div className="col-4">{t('drivertrainschedule:origin')}</div>
<div className="font-weight-bold text-primary col-8">
{data.base.stops.map((stop) => originStop(stop))[0]}
</div>
</div>
<div className="row no-gutters">
<div className="col-4">{t('drivertrainschedule:destination')}</div>
<div className="font-weight-bold text-primary col-8">
{data.base.stops.map((stop) => originStop(stop)).slice(-1)}
</div>
</div>
</div>
<div className="col-xl-4 my-xl-0 my-1">
<div className="row no-gutters">
<div className="col-4 col-xl-5">{t('drivertrainschedule:rollingstock')}</div>
<div className="font-weight-bold text-primary col-8 col-xl-7">
{rollingStockSelected.name}
</div>
</div>
<div className="row no-gutters">
<div className="col-4 col-xl-5">{t('drivertrainschedule:mass')}</div>
<div className="font-weight-bold text-primary col-8 col-xl-7">
{rollingStockSelected.mass}T
</div>
</div>
</div>
<div className="col-xl-4">
<div className="row no-gutters">
<div className="col-4">{t('drivertrainschedule:composition')}</div>
<div className="font-weight-bold text-primary col-8">
{data.speed_limit_composition}
</div>
</div>
</div>
</div>
<div className="text-right font-italic text-cyan">
{t('drivertrainschedule:numberoflines')} :
<span className="font-weight-bold ml-1"> {data.base.stops.length}</span>
</div>
<div className="simulation-drivertrainschedule ml-auto mr-auto">
<table className="table-drivertrainschedule table-hover">
<thead className="bg-light">
<tr>
<th className="text-center text-primary mt-1">
{t('drivertrainschedule:placeholderline')}
</th>
<th colSpan="3" className="text-primary mt-1">
{t('drivertrainschedule:speed')}
</th>
<th className="text-center text-primary mt-1">{t('drivertrainschedule:pk')}</th>
<th className="text-center text-primary mt-1">{t('drivertrainschedule:place')}</th>
<th className="text-center text-primary mt-1">{t('drivertrainschedule:time')}</th>
<th className="text-center text-primary mt-1">
{t('drivertrainschedule:trackname')}
</th>
</tr>
<tr className="text-primary small text-uppercase">
<td />
<th className="font-weight-normal pb-1 text-center">
<span className="d-none d-lg-block d-xl-block">
{t('drivertrainschedule:speedlimit')}
</span>
<span className="d-lg-none d-xl-none">
{t('drivertrainschedule:speedlimit-short')}
</span>
</th>
<th className="font-weight-normal text-center">
<span className="d-none d-lg-block d-xl-block">
{t('drivertrainschedule:actualspeed')}
</span>
<span className="d-lg-none d-xl-none">
{t('drivertrainschedule:actualspeed-short')}
</span>
</th>
<th className="font-weight-normal text-center">
<span className="d-none d-lg-block d-xl-block">
{t('drivertrainschedule:averagespeed')}
</span>
<span className="d-lg-none d-xl-none">
{t('drivertrainschedule:averagespeed-short')}
</span>
</th>
<td colSpan="4" />
</tr>
</thead>
<tbody className="font-weight-normal">
{data.base.stops.map((stop, idx) => formatStops(stop, idx, data))}
</tbody>
</table>
</div>
</div>
);
}

DriverTrainScheduleModal.propTypes = {
data: PropTypes.object.isRequired,
rollingStockSelected: PropTypes.object.isRequired,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { useEffect, useState } from 'react';
import { get } from 'common/requests';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import ModalSNCF from 'common/BootstrapSNCF/ModalSNCF/ModalSNCF';
import ModalBodySNCF from 'common/BootstrapSNCF/ModalSNCF/ModalBodySNCF';
import ModalFooterSNCF from 'common/BootstrapSNCF/ModalSNCF/ModalFooterSNCF';
import DriverTrainScheduleModal from 'applications/osrd/components/DriverTrainSchedule/DriverTrainScheduleModal';

const TRAINSCHEDULE_URL = '/train_schedule';
const ROLLINGSTOCK_URL = '/rolling_stock';

export default function DriverTrainSchedule(props) {
const { t } = useTranslation(['drivertrainschedule', 'translation', 'osrdconf', 'rollingstock']);
const { data } = props;
const [rollingStockSelected, setRollingStockSelected] = useState(undefined);

const getRollingStock = async () => {
try {
const trainScheduleDetails = await get(`${TRAINSCHEDULE_URL}/${data.id}/`);
try {
const rollingStock = await get(
`${ROLLINGSTOCK_URL}/${trainScheduleDetails.rolling_stock}/`
);
setRollingStockSelected(rollingStock);
} catch (e) {
console.log('ERROR', e);
}
} catch (e) {
console.log('ERROR', e);
}
};

useEffect(() => {
if (data) getRollingStock();
}, [data]);

return (
<ModalSNCF className="modal-drivertrainschedule" htmlID="driverTrainScheduleModal" size="xl">
<ModalBodySNCF>
{data && rollingStockSelected ? (
<DriverTrainScheduleModal data={data} rollingStockSelected={rollingStockSelected} />
) : null}
</ModalBodySNCF>
<ModalFooterSNCF>
<div className="d-flex flex-row-reverse w-100">
<button className="btn btn-secondary btn-sm" type="button" data-dismiss="modal">
{t('translation:common.close')}
</button>
</div>
</ModalFooterSNCF>
</ModalSNCF>
);
}

DriverTrainSchedule.propTypes = {
data: PropTypes.object,
};

DriverTrainSchedule.defaultProps = {
data: undefined,
};
Loading

0 comments on commit e56e364

Please sign in to comment.