@@ -20,26 +20,36 @@ import StationSelector from 'modules/trainschedule/components/ImportTimetableIte
2020import { setFailure , setWarning } from 'reducers/main' ;
2121import { useAppDispatch } from 'store' ;
2222import { formatLocalDate } from 'utils/date' ;
23+ import { Duration } from 'utils/duration' ;
2324
2425import { buildSteps , cleanTimeFormat } from './helpers/buildStepsFromOcp' ;
26+ import { findMostFrequentScheduleInPacedTrain } from './helpers/findMostFrequentXmlSchedule' ;
2527import {
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+ } ;
3139interface 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
3847const 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 (
0 commit comments