Skip to content

Commit 8fde8ab

Browse files
committed
front: import missions from xml file
Signed-off-by: romainvalls <[email protected]>
1 parent 33ee30b commit 8fde8ab

File tree

9 files changed

+442
-139
lines changed

9 files changed

+442
-139
lines changed

front/src/applications/operationalStudies/views/ImportTimetableItem.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
ImportTimetableItemConfig,
1313
ImportTimetableItemTrainsList,
1414
} from 'modules/trainschedule/components/ImportTimetableItem';
15+
import type { ImportedPacedTrainSchedule } from 'modules/trainschedule/components/ImportTimetableItem/ImportTimetableItemConfig';
1516
import { setFailure } from 'reducers/main';
1617
import type { TimetableItem } from 'reducers/osrdconf/types';
1718
import { useAppDispatch } from 'store';
@@ -31,6 +32,7 @@ const ImportTimetableItem = ({ timetableId, upsertTimetableItems }: ImportTimeta
3132
paced_trains: [],
3233
});
3334
const [trainsXmlData, setTrainsXmlData] = useState<ImportedTrainSchedule[]>([]);
35+
const [pacedTrainXmlData, setPacedTrainXmlData] = useState<ImportedPacedTrainSchedule[]>([]);
3436

3537
const { data: { results: rollingStocks } = { results: [] }, isError } =
3638
osrdEditoastApi.endpoints.getLightRollingStock.useQuery({
@@ -55,13 +57,15 @@ const ImportTimetableItem = ({ timetableId, upsertTimetableItems }: ImportTimeta
5557
setTrainsList={setTrainsList}
5658
setTrainsJsonData={setTrainsJsonData}
5759
setTrainsXmlData={setTrainsXmlData}
60+
setPacedTrainsXmlData={setPacedTrainXmlData}
5861
/>
5962
<ImportTimetableItemTrainsList
6063
isLoading={isLoading}
6164
timetableId={timetableId}
6265
trainsList={trainsList}
6366
trainsJsonData={trainsJsonData}
6467
trainsXmlData={trainsXmlData}
68+
pacedTrainXmlData={pacedTrainXmlData}
6569
upsertTimetableItems={upsertTimetableItems}
6670
/>
6771
</main>

front/src/modules/trainschedule/components/ImportTimetableItem/ImportTimetableItemConfig.tsx

Lines changed: 147 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,36 @@ import StationSelector from 'modules/trainschedule/components/ImportTimetableIte
2020
import { setFailure, setWarning } from 'reducers/main';
2121
import { useAppDispatch } from 'store';
2222
import { formatLocalDate } from 'utils/date';
23+
import { Duration } from 'utils/duration';
2324

2425
import { buildSteps, cleanTimeFormat } from './helpers/buildStepsFromOcp';
26+
import { findMostFrequentScheduleInPacedTrain } from './helpers/findMostFrequentXmlSchedule';
2527
import {
2628
handleFileReadingError,
2729
processJsonFile,
2830
processXmlFile,
2931
} from '../ManageTrainSchedule/helpers/handleParseFiles';
3032

33+
export type ImportedPacedTrainSchedule = ImportedTrainSchedule & {
34+
paced: {
35+
interval: string;
36+
time_window: string;
37+
};
38+
};
3139
interface ImportTimetableItemConfigProps {
3240
setTrainsList: (trainsList: ImportedTrainSchedule[]) => void;
3341
setIsLoading: (isLoading: boolean) => void;
3442
setTrainsJsonData: (trainsJsonData: TimetableJsonPayload) => void;
3543
setTrainsXmlData: (trainsXmlData: ImportedTrainSchedule[]) => void;
44+
setPacedTrainsXmlData: (pacedTrainsXmlData: ImportedPacedTrainSchedule[]) => void;
3645
}
3746

3847
const ImportTimetableItemConfig = ({
3948
setTrainsList,
4049
setIsLoading,
4150
setTrainsJsonData,
4251
setTrainsXmlData,
52+
setPacedTrainsXmlData,
4353
}: ImportTimetableItemConfigProps) => {
4454
const { t } = useTranslation('operational-studies', { keyPrefix: 'importTrains' });
4555
const [from, setFrom] = useState<ImportStation | undefined>();
@@ -215,7 +225,7 @@ const ImportTimetableItemConfig = ({
215225
return updatedTrainSchedules;
216226
};
217227

218-
const parseRailML = async (xmlDoc: Document): Promise<ImportedTrainSchedule[]> => {
228+
const parseXML = async (xmlDoc: Document): Promise<ImportedTrainSchedule[]> => {
219229
const trainSchedules: ImportedTrainSchedule[] = [];
220230

221231
// Initialize localCichDict
@@ -237,6 +247,10 @@ const ImportTimetableItemConfig = ({
237247
});
238248
});
239249

250+
const pacedTrains: Record<string, ImportedTrainSchedule[]> = {};
251+
const trainGroups = Array.from(xmlDoc.getElementsByTagName('trainGroup'));
252+
253+
const trainSchedulesByTrainPartId: Record<string, ImportedTrainSchedule> = {};
240254
const trainParts = Array.from(xmlDoc.getElementsByTagName('trainPart'));
241255
const period = xmlDoc.getElementsByTagName('timetablePeriod')[0];
242256
const startDate = period ? period.getAttribute('startDate') : null;
@@ -248,9 +262,10 @@ const ImportTimetableItemConfig = ({
248262

249263
trainParts.forEach((train) => {
250264
const trainNumber = train.getAttribute('id') || '';
265+
const trainPartId = train.getAttribute('id') || '';
251266
const ocpSteps = Array.from(train.getElementsByTagName('ocpTT'));
252267
const formationTT = train.getElementsByTagName('formationTT')[0];
253-
const rollingStockViriato = formationTT?.getAttribute('formationRef');
268+
const rollingStockXml = formationTT?.getAttribute('formationRef');
254269
const firstOcpTT = ocpSteps[0];
255270
const firstDepartureTime = firstOcpTT
256271
.getElementsByTagName('times')[0]
@@ -269,18 +284,144 @@ const ImportTimetableItemConfig = ({
269284

270285
const trainSchedule: ImportedTrainSchedule = {
271286
trainNumber,
272-
rollingStock: rollingStockViriato, // RollingStocks in viriato files rarely have the correct format
287+
rollingStock: rollingStockXml, // RollingStocks in xml files rarely have the correct format
273288
departureTime: `${startDate} ${firstDepartureTimeformatted}`,
274289
arrivalTime: `${startDate} ${lastDepartureTimeformatted}`,
275290
departure: '', // Default for testing
276291
steps: adaptedSteps,
277292
};
278-
293+
trainSchedulesByTrainPartId[trainPartId] = trainSchedule;
279294
trainSchedules.push(trainSchedule);
280295
});
296+
297+
const trainElementsById: Record<string, Element> = {};
298+
Array.from(xmlDoc.getElementsByTagName('train')).forEach((train) => {
299+
const id = train.getAttribute('id');
300+
if (id) {
301+
trainElementsById[id] = train;
302+
}
303+
});
304+
305+
trainGroups.forEach((trainGroup) => {
306+
const pacedTrainId = trainGroup.getAttribute('id')!;
307+
308+
const trainRefs = Array.from(trainGroup.getElementsByTagName('trainRef'));
309+
pacedTrains[pacedTrainId] = trainRefs
310+
.map((trainRef) => {
311+
const trainId = trainRef.getAttribute('ref');
312+
const trainElement = trainId ? trainElementsById[trainId] : undefined;
313+
314+
const trainPartRef = trainElement?.querySelector('trainPartRef')?.getAttribute('ref');
315+
316+
return trainPartRef ? trainSchedulesByTrainPartId[trainPartRef] : undefined;
317+
})
318+
.filter((schedule) => schedule !== undefined);
319+
});
320+
321+
const pacedTrainMostFrequentSchedules: Record<
322+
string,
323+
{ schedule: ImportedTrainSchedule | null; count: number }
324+
> = {};
325+
326+
Object.entries(pacedTrains).forEach(([pacedTrainId, schedules]) => {
327+
const { mostFrequent, highestCount } = findMostFrequentScheduleInPacedTrain(schedules);
328+
pacedTrainMostFrequentSchedules[pacedTrainId] = {
329+
schedule: mostFrequent,
330+
count: highestCount,
331+
};
332+
});
333+
334+
const getMostFrequentInterval = (schedules: ImportedTrainSchedule[]): Duration => {
335+
const departureTimes = schedules
336+
.map((s) => new Date(s.departureTime))
337+
.sort((a, b) => a.getTime() - b.getTime());
338+
339+
const intervalsCount = new Map<number, number>();
340+
341+
for (let i = 1; i < departureTimes.length; i += 1) {
342+
const interval = Duration.subtractDate(departureTimes[i], departureTimes[i - 1]);
343+
const rawMin = interval.total('minute');
344+
345+
let roundedMin: number;
346+
if (rawMin > 5) {
347+
roundedMin = Math.round(rawMin / 10) * 10;
348+
} else if (rawMin >= 1) {
349+
roundedMin = Math.round(rawMin);
350+
} else {
351+
roundedMin = 1;
352+
}
353+
354+
intervalsCount.set(roundedMin, (intervalsCount.get(roundedMin) || 0) + 1);
355+
}
356+
357+
let mostFrequentRoundedMin = 0;
358+
let maxCount = 0;
359+
360+
for (const [minutes, count] of intervalsCount.entries()) {
361+
if (count > maxCount) {
362+
mostFrequentRoundedMin = minutes;
363+
maxCount = count;
364+
} else if (count === maxCount && minutes < mostFrequentRoundedMin) {
365+
// we take smaller interval in case of tie
366+
mostFrequentRoundedMin = minutes;
367+
}
368+
}
369+
370+
return new Duration({ minutes: mostFrequentRoundedMin });
371+
};
372+
373+
const buildPacedTrain = (
374+
pacedTrainId: string,
375+
pacedTrainSchedules: ImportedTrainSchedule[]
376+
): ImportedPacedTrainSchedule | null => {
377+
if (pacedTrainSchedules.length < 2) {
378+
console.warn('Not enough schedules to build a paced train');
379+
return null;
380+
}
381+
382+
const sortedSchedules = pacedTrainSchedules.sort(
383+
(a, b) => new Date(a.departureTime).getTime() - new Date(b.departureTime).getTime()
384+
);
385+
386+
const departureDates = sortedSchedules.map((s) => new Date(s.departureTime));
387+
const intervalDuration = getMostFrequentInterval(pacedTrainSchedules);
388+
389+
const totalDuration = Duration.subtractDate(
390+
departureDates[departureDates.length - 1],
391+
departureDates[0]
392+
).add(intervalDuration);
393+
394+
return {
395+
...sortedSchedules[0],
396+
trainNumber: pacedTrainId,
397+
paced: {
398+
interval: intervalDuration.toISOString(),
399+
time_window: totalDuration.toISOString(),
400+
},
401+
};
402+
};
403+
404+
const importedPacedTrains: ImportedPacedTrainSchedule[] = Object.entries(pacedTrains)
405+
.map(([pacedTrainId, pacedTrainSchedules]) =>
406+
buildPacedTrain(pacedTrainId, pacedTrainSchedules)
407+
)
408+
.filter((pacedTrain) => pacedTrain !== null);
409+
410+
setPacedTrainsXmlData(importedPacedTrains);
411+
281412
const trains = Array.from(xmlDoc.getElementsByTagName('train'));
282413
const updatedTrainSchedules = mapTrainNames(trainSchedules, trains);
283-
setTrainsXmlData(updatedTrainSchedules);
414+
const trainSchedulesInPacedTrain = new Set(
415+
Object.values(pacedTrains)
416+
.flat()
417+
.map((schedule) => schedule.trainNumber)
418+
);
419+
420+
const singleTrainSchedules = trainSchedules.filter(
421+
(schedule) => !trainSchedulesInPacedTrain.has(schedule.trainNumber)
422+
);
423+
setTrainsXmlData(singleTrainSchedules);
424+
284425
return updatedTrainSchedules;
285426
};
286427

@@ -311,7 +452,7 @@ const ImportTimetableItemConfig = ({
311452

312453
// try to parse the file as an XML file
313454
try {
314-
await processXmlFile(fileContent, parseRailML, updateTrainSchedules);
455+
await processXmlFile(fileContent, parseXML, updateTrainSchedules);
315456
} catch {
316457
// the file is not supported or is an invalid XML file
317458
dispatch(

front/src/modules/trainschedule/components/ImportTimetableItem/ImportTimetableItemTrainsList.tsx

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import type {
2020
import { useAppDispatch } from 'store';
2121
import { formatEditoastIdToPacedTrainId, formatEditoastIdToTrainScheduleId } from 'utils/trainId';
2222

23-
import { generateTrainSchedulesPayloads } from './generateTrainSchedulesPayloads';
23+
import {
24+
generatePacedTrainPayloads,
25+
generateTrainSchedulesPayloads,
26+
} from './generateTrainSchedulesPayloads';
27+
import type { ImportedPacedTrainSchedule } from './ImportTimetableItemConfig';
2428
import findValidTrainNameKey from '../ManageTrainSchedule/helpers/findValidTrainNameKey';
2529

2630
function LoadingIfSearching({
@@ -44,6 +48,7 @@ type ImportTimetableItemTrainsListProps = {
4448
trainsJsonData: TimetableJsonPayload;
4549
trainsXmlData: ImportedTrainSchedule[];
4650
upsertTimetableItems: (timetableItems: TimetableItem[]) => void;
51+
pacedTrainXmlData: ImportedPacedTrainSchedule[];
4752
};
4853

4954
const ImportTimetableItemTrainsList = ({
@@ -53,12 +58,12 @@ const ImportTimetableItemTrainsList = ({
5358
trainsJsonData,
5459
trainsXmlData,
5560
upsertTimetableItems,
61+
pacedTrainXmlData,
5662
}: ImportTimetableItemTrainsListProps) => {
5763
const { t } = useTranslation('operational-studies', { keyPrefix: 'importTrains' });
5864

5965
const { train_schedules: trainSchedulesJsonData, paced_trains: pacedTrainsJsonData } =
6066
trainsJsonData;
61-
6267
const formattedTrainsList = useMemo(
6368
() =>
6469
trainsList.map(({ rollingStock, ...train }) => {
@@ -87,9 +92,9 @@ const ImportTimetableItemTrainsList = ({
8792
let trainSchedulePayloads: TrainSchedule[] = [];
8893
let pacedTrainPayloads: PacedTrain[] = [];
8994

90-
// Viriato import (TODO Paced train : handle viriato imports for paced trains)
95+
// XML import
9196
if (trainsXmlData.length > 0) {
92-
trainSchedulePayloads = generateTrainSchedulesPayloads(trainsXmlData, true);
97+
trainSchedulePayloads = generateTrainSchedulesPayloads(trainsXmlData);
9398

9499
// JSON import
95100
} else if (trainSchedulesJsonData.length > 0 || pacedTrainsJsonData.length > 0) {
@@ -98,7 +103,11 @@ const ImportTimetableItemTrainsList = ({
98103

99104
// Open data import (only handle trainSchedules)
100105
} else {
101-
trainSchedulePayloads = generateTrainSchedulesPayloads(formattedTrainsList, false);
106+
trainSchedulePayloads = generateTrainSchedulesPayloads(formattedTrainsList);
107+
}
108+
109+
if (pacedTrainXmlData.length > 0) {
110+
pacedTrainPayloads = generatePacedTrainPayloads(pacedTrainXmlData);
102111
}
103112

104113
let formattedTrainSchedules: TrainScheduleResponseWithTrainId[] = [];
@@ -154,18 +163,29 @@ const ImportTimetableItemTrainsList = ({
154163
}
155164

156165
const computedItemImportLabel = () => {
157-
if (!trainSchedulesJsonData.length && !trainsList.length && !!pacedTrainsJsonData.length) {
166+
if (
167+
!trainSchedulesJsonData.length &&
168+
!trainsList.length &&
169+
(!!pacedTrainsJsonData.length || !!pacedTrainXmlData)
170+
) {
158171
return t('pacedTrainsFound', {
159-
count: pacedTrainsJsonData.length,
160-
pacedTrainsFound: pacedTrainsJsonData.length,
172+
count: pacedTrainsJsonData.length || pacedTrainXmlData.length,
173+
pacedTrainsFound: pacedTrainsJsonData.length || pacedTrainXmlData.length,
161174
});
162175
}
163176

164177
return t('itemsFound', {
165-
count: trainsList.length || [...trainSchedulesJsonData, ...pacedTrainsJsonData].length,
166-
pacedTrainsFound: pacedTrainsJsonData.length,
167-
trainsFound: trainsList.length || trainSchedulesJsonData.length,
168-
and: !!trainSchedulesJsonData.length && !!pacedTrainsJsonData.length ? t('and') : '',
178+
count:
179+
trainsList.length ||
180+
[...trainSchedulesJsonData, ...pacedTrainsJsonData, ...trainsXmlData, ...pacedTrainXmlData]
181+
.length,
182+
pacedTrainsFound: pacedTrainsJsonData.length || pacedTrainXmlData.length,
183+
trainsFound: trainsList.length || trainSchedulesJsonData.length || trainsXmlData.length,
184+
and:
185+
(!!trainSchedulesJsonData.length && !!pacedTrainsJsonData.length) ||
186+
(!!trainsXmlData.length && !!pacedTrainXmlData)
187+
? t('and')
188+
: '',
169189
});
170190
};
171191

0 commit comments

Comments
 (0)