Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

front: import - autocomplete pathfings with waypoints #4422

Merged
merged 1 commit into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions editoast/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ paths:
/search/:
post:
summary: Generic search endpoint
parameters:
- in: query
name: page_size
schema:
type: integer
description: number of results
requestBody:
description: Search query
required: true
Expand Down Expand Up @@ -2556,6 +2562,7 @@ components:
format: float
description: The offset on the track section
example: 42.
required: [track, offset]

Direction:
type: string
Expand Down Expand Up @@ -3101,22 +3108,14 @@ components:
type: string
ch:
type: string
track_sections:
type: array
items:
type: object
required:
- track
- position
properties:
track:
type: string
position:
type: integer
geographic:
$ref: "#/components/schemas/MultiPoint"
schematic:
$ref: "#/components/schemas/MultiPoint"
track_sections:
type: array
items:
$ref: "#/components/schemas/TrackLocation"

SearchTrackResult:
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"inputPlaceholder": "Name, trigram",
"launchImport": "Start import",
"noResults": "No results",
"or": "or",
"startTime": "START",
"status": {
"calculatingTrainSchedule": "Calculating train schedules...",
Expand All @@ -33,6 +34,7 @@
"missingTimetable": "You must choose a timetable",
"noImportationPossible": "Import is currently not available:",
"pathComplete": "Pathfinding complete",
"pathsFailed": "All the pathfindings have failed",
"ready": "Ready.",
"searchingPath": "Search for a path",
"uicComplete": "All locations have been completed."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,29 @@
"unableToRetrieveTrainSchedule": "Impossible de réaliser les calculs de marche"
},
"from": "Origine",
"generatePaths": "Générer les pathfinding",
"generatePathsAuto": "Compléter et générer les pathfinding automatiquement",
"generatePaths": "Générer les itinéraires",
"generatePathsAuto": "Compléter et générer les itinéraires automatiquement",
"generateTrainSchedules": "Générer les calculs de marches",
"goToSTDCM": "Aller aux sillons de dernière minute",
"import": "Importation",
"inputPlaceholder": "Nom, trigramme",
"launchImport": "Démarrer l'importation",
"noResults": "Aucun résultat",
"or": "ou",
"startTime": "DÉBUT",
"status": {
"calculatingTrainSchedule": "Calculs de marches en cours…",
"calculatingTrainScheduleComplete": "Calculs de marches terminés",
"calculatingTrainScheduleCompleteAll": "Tous les calculs de marches sont terminés.",
"complete": "positionnez",
"complete": "{{uicNumber}}/{{uicTotalCount}} positionnez {{uicName}}",
"missingInfra": "Vous devez renseigner une infrastructure",
"missingRollingStock": "Vous devez choisir un matériel roulant par défaut",
"missingTimetable": "Vous devez choisir une grille horaire",
"noImportationPossible": "L'importation n'est pas possible pour l'instant :",
"pathComplete": "Tous les pathfinding ont été effectués",
"pathComplete": "Toutes les recherches d'itinéraires ont été effectués",
"pathsFailed": "Toutes les recherches d'itinéraires ont échoué",
"ready": "Prêt.",
"searchingPath": "Génération pathfinding",
"searchingPath": "{{pathFindingsDoneCount}}/{{pathFindingsDoneCount}} Génération des itinéraires {{pathRef}}",
"uicComplete": "Tous les lieux ont été renseignés."
},
"to": "Destination",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
import { isEmpty } from 'lodash';
import React, { useState, useContext } from 'react';
import { useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';

import RollingStockSelector from 'common/RollingStockSelector/WithRollingStockSelector';
import PropTypes from 'prop-types';
import InputSNCF from 'common/BootstrapSNCF/InputSNCF';
import MemoStationSelector from 'applications/operationalStudies/components/ImportTrainSchedule/ImportTrainScheduleStationSelector';
import { setFailure } from 'reducers/main';
import { useDispatch } from 'react-redux';
import StationCard from 'common/StationCard';
import { formatIsoDate } from 'utils/date';
import UploadFileModal from 'applications/customget/components/uploadFileModal';
import { ModalContext } from 'common/BootstrapSNCF/ModalSNCF/ModalProvider';
import { TrainSchedule, TrainScheduleImportConfig } from 'applications/operationalStudies/types';
import {
ImportedTrainSchedule,
Step,
TrainSchedule,
TrainScheduleImportConfig,
} from 'applications/operationalStudies/types';
import {
SearchOperationalPointResult,
PostSearchApiArg,
osrdEditoastApi,
TrackLocation,
} from 'common/api/osrdEditoastApi';
import { getGraouTrainSchedules } from 'common/api/graouApi';

interface ImportTrainScheduleConfigProps {
setConfig: (config: TrainScheduleImportConfig) => void;
infraId: number;
setTrainsList: (trainsList: TrainSchedule[]) => void;
setIsLoading: (isLoading: boolean) => void;
}

export default function ImportTrainScheduleConfig(props: ImportTrainScheduleConfigProps) {
const { setConfig, setTrainsList } = props;
type SearchConstraintType = (string | number | string[])[];

export default function ImportTrainScheduleConfig({
setTrainsList,
infraId,
setIsLoading,
}: ImportTrainScheduleConfigProps) {
const { t } = useTranslation(['operationalStudies/importTrainSchedule']);
const [postSearch] = osrdEditoastApi.usePostSearchMutation();
const [from, setFrom] = useState();
const [fromSearchString, setFromSearchString] = useState('');
const [to, setTo] = useState();
Expand All @@ -30,25 +50,134 @@ export default function ImportTrainScheduleConfig(props: ImportTrainScheduleConf
const dispatch = useDispatch();
const { openModal, closeModal } = useContext(ModalContext);

async function importTrackSections(opIds: number[]): Promise<Record<number, TrackLocation[]>> {
const uniqueOpIds = Array.from(new Set(opIds));
const constraint = uniqueOpIds.reduce((res, uic) => [...res, ['=', ['uic'], Number(uic)]], [
'or',
] as (string | SearchConstraintType)[]);
const payload: PostSearchApiArg = {
body: {
object: 'operationalpoint',
query: ['and', constraint, ['=', ['infra_id'], infraId]],
},
pageSize: 1000,
};
const operationalPoints = (await postSearch(
payload
).unwrap()) as SearchOperationalPointResult[];

return operationalPoints.reduce(
(res, operationalPoint) =>
operationalPoint.uic
? {
...res,
[operationalPoint.uic]: operationalPoint.track_sections,
}
: res,
{}
);
}

function validateImportedTrainSchedules(
importedTrainSchedules: Record<string, unknown>[]
): ImportedTrainSchedule[] | null {
const isInvalidTrainSchedules = importedTrainSchedules.some((trainSchedule) => {
if (
['trainNumber', 'rollingStock', 'departureTime', 'arrivalTime', 'departure', 'steps'].some(
(key) => !(key in trainSchedule)
) ||
!Array.isArray(trainSchedule.steps)
) {
return true;
}
const hasInvalidteps = trainSchedule.steps.some((step) =>
[
'arrivalTime',
'departureTime',
'uic',
'yard',
'name',
'trigram',
'latitude',
'longitude',
].some((key) => !(key in step))
);
return hasInvalidteps;
});
if (isInvalidTrainSchedules) {
dispatch(
setFailure({
name: t('errorMessages.error'),
message: 'Impossible de convertir les données en TrainSchedule',
})
);
console.error(
'Invalid data format: can not convert response into TrainSchedules. Expected format : { trainNumber: string; rollingStock: string; departureTime: string; arrivalTime: string; departure: string; steps: ({uic: number; yard: string; name: string; trigram: string; latitude: number; longitude: number; arrivalTime: string; departureTime: string; })[]; transilienName?: string; }'
);
return null;
}
return importedTrainSchedules as ImportedTrainSchedule[];
}

async function updateTrainSchedules(importedTrainSchedules: ImportedTrainSchedule[]) {
const opIds = importedTrainSchedules.flatMap((trainSchedule) =>
trainSchedule.steps.map((step) => step.uic)
);
const trackSectionsByOp = await importTrackSections(opIds);

// For each train schedule, we add the duration and tracks of each step
const trainsSchedules = importedTrainSchedules.map((trainSchedule) => {
const stepsWithDuration = trainSchedule.steps.map((step) => {
// calcul duration in seconds between step arrival and departure
// in case of arrival and departure are the same, we set duration to 0
// for the step arrivalTime is before departureTime because the train first goes to the station and then leaves it
const duration = Math.round(
(new Date(step.departureTime).getTime() - new Date(step.arrivalTime).getTime()) / 1000
);
return {
...step,
duration,
tracks: trackSectionsByOp[Number(step.uic)] || [],
} as Step;
});
return {
...trainSchedule,
steps: stepsWithDuration,
} as TrainSchedule;
});

setTrainsList(trainsSchedules);
}

async function getTrainsFromOpenData(config: TrainScheduleImportConfig) {
setTrainsList([]);
setIsLoading(true);

const result = await getGraouTrainSchedules(config);
const importedTrainSchedules = validateImportedTrainSchedules(result);
if (importedTrainSchedules && !isEmpty(importedTrainSchedules)) {
await updateTrainSchedules(importedTrainSchedules);
}

setIsLoading(false);
}

function defineConfig() {
let error = false;
if (!from) {
dispatch(
setFailure({ name: t('errorMessages.error'), message: t('errorMessages.errorNoFrom') })
);
error = true;
}
if (!to) {
dispatch(
setFailure({ name: t('errorMessages.error'), message: t('errorMessages.errorNoTo') })
);
error = true;
}
if (!date) {
dispatch(
setFailure({ name: t('errorMessages.error'), message: t('errorMessages.errorNoDate') })
);
error = true;
}
if (JSON.stringify(from) === JSON.stringify(to)) {
dispatch(
Expand All @@ -57,43 +186,26 @@ export default function ImportTrainScheduleConfig(props: ImportTrainScheduleConf
error = true;
}

if (!error) {
setConfig({
if (from && to && date && !error) {
getTrainsFromOpenData({
from,
to,
date,
startTime,
endTime,
});
} as TrainScheduleImportConfig);
}
}

const importFile = async (file: File) => {
closeModal();
if (file) {
const text = await file.text();
const trainsSchedulesTemp: TrainSchedule[] = JSON.parse(text);
// For each train schedule, we add the duration of each step
const trainsSchedules = trainsSchedulesTemp.map((trainSchedule) => {
const stepsWithDuration = trainSchedule.steps.map((step) => {
// calcul duration in seconds between step arrival and departure
// in case of arrival and departure are the same, we set duration to 0
// for the step arrivalTime is before departureTime because the train first goes to the station and then leaves it
const duration = Math.round(
(new Date(step.departureTime).getTime() - new Date(step.arrivalTime).getTime()) / 1000
);
return {
...step,
duration,
};
});
return {
...trainSchedule,
steps: stepsWithDuration,
};
});
setTrainsList([]);

setTrainsList(trainsSchedules);
const text = await file.text();
const importedTrainSchedules = validateImportedTrainSchedules(JSON.parse(text));

if (importedTrainSchedules && !isEmpty(importedTrainSchedules)) {
await updateTrainSchedules(importedTrainSchedules);
}
};

Expand Down Expand Up @@ -223,8 +335,3 @@ export default function ImportTrainScheduleConfig(props: ImportTrainScheduleConf
</>
);
}

ImportTrainScheduleConfig.propTypes = {
setConfig: PropTypes.func.isRequired,
setTrainsList: PropTypes.func.isRequired,
};
Loading