-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
front: improved driver train schedule (#2409)
- Loading branch information
1 parent
ca42ea7
commit e56e364
Showing
6 changed files
with
437 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": "N°", | ||
"rollingstock": "Matériel roulant", | ||
"speed": "Vitesse", | ||
"speedLimitByTag": "Code de composition", | ||
"speedlimit": "Limite", | ||
"speedlimit-short": "Lim.", | ||
"time": "Horaire", | ||
"timetable": "Table horaire", | ||
"trackname": "Voie" | ||
} |
172 changes: 172 additions & 0 deletions
172
front/src/applications/osrd/components/DriverTrainSchedule/DriverTrainScheduleHelpers.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
119 changes: 119 additions & 0 deletions
119
front/src/applications/osrd/components/DriverTrainSchedule/DriverTrainScheduleModal.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
62 changes: 62 additions & 0 deletions
62
front/src/applications/osrd/views/OSRDSimulation/DriverTrainSchedule.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
Oops, something went wrong.