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: stdcm: linked train search improvements #9571

Merged
merged 1 commit into from
Dec 16, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,22 @@ const StdcmLinkedPathSearch = ({

const {
displaySearchButton,
hasSearchBeenLaunched,
launchTrainScheduleSearch,
linkedPathDate,
linkedPathResults,
resetLinkedPathSearch,
selectableSlot,
setDisplaySearchButton,
setLinkedPathDate,
setTrainNameInput,
trainNameInput,
} = useLinkedPathSearch();

const removeLinkedPathCard = () => {
setShowLinkedPathSearch(false);
resetLinkedPathSearch();
};

return (
<div className={`stdcm-linked-path-search-container ${className}`}>
{!displayLinkedPathSearch ? (
Expand All @@ -59,7 +64,7 @@ const StdcmLinkedPathSearch = ({
disabled={disabled}
name={cardName}
title={
<button type="button" onClick={() => setShowLinkedPathSearch(false)}>
<button type="button" onClick={removeLinkedPathCard}>
{t('translation:common.delete').toLowerCase()}
</button>
}
Expand Down Expand Up @@ -98,18 +103,17 @@ const StdcmLinkedPathSearch = ({
{t('find')}
</button>
)}
{!displaySearchButton && !linkedPathResults.length && (
{!displaySearchButton && !linkedPathResults && (
<div className="stdcm-linked-path-button white">
<Gear size="lg" className="stdcm-linked-path-loading" />
</div>
)}
{linkedPathResults.length > 0 ? (
<StdcmLinkedPathResults linkedPathResults={linkedPathResults} linkedOp={linkedOp} />
) : (
hasSearchBeenLaunched && (
{linkedPathResults &&
(linkedPathResults.length > 0 ? (
<StdcmLinkedPathResults linkedPathResults={linkedPathResults} linkedOp={linkedOp} />
) : (
<p className="text-center mb-0">{t('noCorrespondingResults')}</p>
)
)}
))}
</StdcmCard>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useEffect, useMemo } from 'react';

import { DatePicker, Select, TimePicker, TolerancePicker } from '@osrd-project/ui-core';
import { useTranslation } from 'react-i18next';
Expand Down Expand Up @@ -38,9 +38,7 @@ type StdcmOpScheduleProps = {

const defaultDate = (date?: Date) => {
const newDate = date ? new Date(date) : new Date();
newDate.setHours(0);
newDate.setMinutes(0);
newDate.setSeconds(0);
newDate.setHours(0, 0, 0);
return newDate;
};

Expand All @@ -63,13 +61,13 @@ const StdcmOpSchedule = ({
useMemo(() => {
const isArrivalDateValid =
opTimingData?.arrivalDate &&
isArrivalDateInSearchTimeWindow(opTimingData.arrivalDate, searchDatetimeWindow);
isArrivalDateInSearchTimeWindow(new Date(opTimingData.arrivalDate), searchDatetimeWindow);
return {
arrivalDate:
opTimingData && isArrivalDateValid
? new Date(opTimingData.arrivalDate)
: defaultDate(searchDatetimeWindow?.begin),
arrivalTime: opTimingData?.arrivalTime || '--:--',
arrivalTime: opTimingData?.arrivalTime,
arrivalTimeHours: opTimingData?.arrivalTimehours,
arrivalTimeMinutes: opTimingData?.arrivalTimeMinutes,
arrivalToleranceValues: {
Expand Down Expand Up @@ -101,6 +99,20 @@ const StdcmOpSchedule = ({
[t, searchDatetimeWindow]
);

useEffect(() => {
if (
(!isArrivalDateInSearchTimeWindow(arrivalDate, searchDatetimeWindow) ||
!opTimingData?.arrivalDate) &&
opScheduleTimeType === 'preciseTime'
) {
onArrivalChange({
date: defaultDate(searchDatetimeWindow?.begin),
hours: arrivalTimeHours || 0,
minutes: arrivalTimeMinutes || 0,
});
}
}, [searchDatetimeWindow, opScheduleTimeType]);

return (
<>
<div className="arrival-type-select">
Expand Down
118 changes: 81 additions & 37 deletions front/src/applications/stdcm/hooks/useLinkedPathSearch.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import { useMemo, useState, useCallback } from 'react';
import { useMemo, useState, useCallback, useEffect } from 'react';

import { compact, groupBy } from 'lodash';
import { compact } from 'lodash';
import { useSelector } from 'react-redux';

import type { PathItem, SearchResultItemTrainSchedule } from 'common/api/osrdEditoastApi';
import type {
PathItem,
SearchQuery,
SearchResultItemOperationalPoint,
SearchResultItemTrainSchedule,
} from 'common/api/osrdEditoastApi';
import { osrdEditoastApi } from 'common/api/osrdEditoastApi';
import { useOsrdConfSelectors } from 'common/osrdContext';
import { isEqualDate } from 'utils/date';
import { useInfraID, useOsrdConfSelectors } from 'common/osrdContext';
import { isArrivalDateInSearchTimeWindow, isEqualDate } from 'utils/date';

import type { StdcmLinkedPathResult, StdcmLinkedPathStep } from '../types';
import type { StdcmLinkedPathResult } from '../types';
import computeOpSchedules from '../utils/computeOpSchedules';

const useLinkedPathSearch = () => {
const [postSearch] = osrdEditoastApi.endpoints.postSearch.useMutation();
const [postTrainScheduleSimulationSummary] =
osrdEditoastApi.endpoints.postTrainScheduleSimulationSummary.useLazyQuery();

const { getTimetableID, getSearchDatetimeWindow } = useOsrdConfSelectors();

const infraId = useInfraID();
const timetableId = useSelector(getTimetableID);
const searchDatetimeWindow = useSelector(getSearchDatetimeWindow);

Expand All @@ -29,42 +37,60 @@ const useLinkedPathSearch = () => {
}, [searchDatetimeWindow]);

const [displaySearchButton, setDisplaySearchButton] = useState(true);
const [hasSearchBeenLaunched, setHasSearchBeenLaunched] = useState(false);
const [trainNameInput, setTrainNameInput] = useState('');
const [linkedPathDate, setLinkedPathDate] = useState(selectableSlot.start);
const [linkedPathResults, setLinkedPathResults] = useState<StdcmLinkedPathResult[]>([]);
const [linkedPathResults, setLinkedPathResults] = useState<StdcmLinkedPathResult[]>();

const getExtremitiesDetails = useCallback(
async (pathItemList: PathItem[]) => {
const origin = pathItemList.at(0)!;
const destination = pathItemList.at(-1)!;
if (!('operational_point' in origin) || !('operational_point' in destination))
return undefined;
const originId = origin.operational_point;
const destinationId = destination.operational_point;
const getExtremityDetails = useCallback(
async (pathItem: PathItem) => {
if (!('operational_point' in pathItem) && !('uic' in pathItem)) return undefined;

const pathItemQuery =
'operational_point' in pathItem
? ['=', ['obj_id'], pathItem.operational_point]
: ([
'and',
['=', ['uic'], pathItem.uic],
['=', ['ch'], pathItem.secondary_code],
] as SearchQuery);

try {
const payloadOP = {
object: 'operationalpoint',
query: ['or', ['=', ['obj_id'], originId], ['=', ['obj_id'], destinationId]],
};
const resultsOP = await postSearch({ searchPayload: payloadOP, pageSize: 25 }).unwrap();
const groupedResults = groupBy(resultsOP, 'obj_id');
return {
origin: groupedResults[originId][0],
destination: groupedResults[destinationId][0],
query: pathItemQuery,
};
const opDetails = (await postSearch({
searchPayload: payloadOP,
pageSize: 25,
}).unwrap()) as SearchResultItemOperationalPoint[];
return opDetails[0];
} catch (error) {
console.error('Failed to fetch operational points:', error);
console.error('Failed to fetch operational point:', error);
return undefined;
}
},
[postSearch]
);

const getTrainsSummaries = useCallback(
async (trainsIds: number[]) => {
if (!infraId) return undefined;
const trainsSummaries = await postTrainScheduleSimulationSummary({
body: {
infra_id: infraId,
ids: trainsIds,
},
}).unwrap();
return trainsSummaries;
},
[postTrainScheduleSimulationSummary, infraId]
);

const launchTrainScheduleSearch = useCallback(async () => {
setLinkedPathResults(undefined);
if (!trainNameInput) return;

setDisplaySearchButton(false);
setLinkedPathResults([]);
try {
const results = (await postSearch({
searchPayload: {
Expand All @@ -86,41 +112,59 @@ const useLinkedPathSearch = () => {
return;
}

const filteredResultsSummaries = await getTrainsSummaries(filteredResults.map((r) => r.id));

const newLinkedPathResults = await Promise.all(
filteredResults.map(async (result) => {
const opDetails = await getExtremitiesDetails(result.path);
const computedOpSchedules = computeOpSchedules(
result.start_time,
result.schedule.at(-1)!.arrival!
);
if (opDetails === undefined) return undefined;
const resultSummary = filteredResultsSummaries && filteredResultsSummaries[result.id];
if (!resultSummary || resultSummary.status !== 'success') return undefined;
const msFromStartTime = resultSummary.path_item_times_final.at(-1)!;

const originDetails = await getExtremityDetails(result.path.at(0)!);
const destinationDetails = await getExtremityDetails(result.path.at(-1)!);
const computedOpSchedules = computeOpSchedules(result.start_time, msFromStartTime);

if (!originDetails || !destinationDetails) return undefined;
return {
trainName: result.train_name,
origin: { ...opDetails.origin, ...computedOpSchedules.origin } as StdcmLinkedPathStep,
origin: { ...originDetails, ...computedOpSchedules.origin },
destination: {
...opDetails.destination,
...destinationDetails,
...computedOpSchedules.destination,
} as StdcmLinkedPathStep,
},
};
})
);
setLinkedPathResults(compact(newLinkedPathResults));
setHasSearchBeenLaunched(true);
} catch (error) {
console.error('Train schedule search failed:', error);
setDisplaySearchButton(true);
}
}, [postSearch, trainNameInput, timetableId, linkedPathDate, getExtremitiesDetails]);
}, [postSearch, trainNameInput, timetableId, linkedPathDate, getExtremityDetails]);

const resetLinkedPathSearch = () => {
setDisplaySearchButton(true);
setLinkedPathResults(undefined);
setTrainNameInput('');
};

useEffect(() => {
if (!isArrivalDateInSearchTimeWindow(linkedPathDate, searchDatetimeWindow)) {
setLinkedPathDate(selectableSlot.start);
resetLinkedPathSearch();
}
}, [selectableSlot]);

return {
displaySearchButton,
hasSearchBeenLaunched,
launchTrainScheduleSearch,
linkedPathDate,
linkedPathResults,
resetLinkedPathSearch,
selectableSlot,
setDisplaySearchButton,
setLinkedPathDate,
setLinkedPathResults,
setTrainNameInput,
trainNameInput,
};
Expand Down
22 changes: 18 additions & 4 deletions front/src/applications/stdcm/utils/computeOpSchedules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,26 @@ import {
substractDurationToIsoDate,
} from 'utils/date';

const computeOpSchedules = (startTime: string, secondsFromStartTime: string) => {
/**
* Computes the operation schedules for a given start time and duration.
*
* @param startTime - The ISO string representing the start time.
* @param msFromStartTime - The duration in milliseconds from the start time.
* @returns An object containing the origin and destination schedules.
*
* The function extracts the date and time from the provided ISO start time and calculates the destination arrival time
* by adding the specified duration. It then returns an object with the origin and destination schedules, including
* the date, time, and ISO arrival times.
*
* Note: A margin of 1800 seconds (30 minutes) is applied to the departure and arrival times to allow for necessary
* activities such as preparation for the next departure.
*/
const computeOpSchedules = (startTime: string, msFromStartTime: number) => {
const { arrivalDate: originDate, arrivalTime: originTime } = extractDateAndTimefromISO(
startTime,
'DD/MM/YY'
);
const destinationArrivalTime = addDurationToIsoDate(startTime, secondsFromStartTime);
const destinationArrivalTime = addDurationToIsoDate(startTime, msFromStartTime, 'millisecond');
const { arrivalDate: destinationDate, arrivalTime: destinationTime } = extractDateAndTimefromISO(
destinationArrivalTime,
'DD/MM/YY'
Expand All @@ -19,12 +33,12 @@ const computeOpSchedules = (startTime: string, secondsFromStartTime: string) =>
origin: {
date: originDate,
time: originTime,
isoArrivalTime: substractDurationToIsoDate(startTime, 'PT1800S'),
isoArrivalTime: substractDurationToIsoDate(startTime, 1800),
},
destination: {
date: destinationDate,
time: destinationTime,
isoArrivalTime: addDurationToIsoDate(destinationArrivalTime, 'PT1800S'),
isoArrivalTime: addDurationToIsoDate(destinationArrivalTime, 1800),
},
};
};
Expand Down
12 changes: 9 additions & 3 deletions front/src/utils/__tests__/date.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ describe('extractDateAndTimefromISO', () => {

describe('isArrivalDateInSearchTimeWindow', () => {
it('should return true if searchDatetimeWindow is undefined', () => {
const result = isArrivalDateInSearchTimeWindow('2024-08-01T10:00:00Z', undefined);
const result = isArrivalDateInSearchTimeWindow(new Date('2024-08-01T10:00:00Z'), undefined);
expect(result).toBe(true);
});

Expand All @@ -104,7 +104,10 @@ describe('isArrivalDateInSearchTimeWindow', () => {
begin: new Date('2024-08-01T00:00:00Z'),
end: new Date('2024-08-02T00:00:00Z'),
};
const result = isArrivalDateInSearchTimeWindow('2024-08-01T10:00:00Z', searchDatetimeWindow);
const result = isArrivalDateInSearchTimeWindow(
new Date('2024-08-01T10:00:00Z'),
searchDatetimeWindow
);
expect(result).toBe(true);
});

Expand All @@ -113,7 +116,10 @@ describe('isArrivalDateInSearchTimeWindow', () => {
begin: new Date('2024-08-01T00:00:00Z'),
end: new Date('2024-08-02T00:00:00Z'),
};
const result = isArrivalDateInSearchTimeWindow('2024-07-30T23:59:59Z', searchDatetimeWindow);
const result = isArrivalDateInSearchTimeWindow(
new Date('2024-07-30T23:59:59Z'),
searchDatetimeWindow
);
expect(result).toBe(false);
});
});
Expand Down
Loading
Loading