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

Type and path faster than light #5870

Merged
merged 2 commits into from
Nov 28, 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
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
"gare": "Station, post",
"importTrainSchedule": "Import",
"infraLoading": "Infrastructure is loading…",
"inputOPTrigrams": "Enter the trigrams of operational points you want to the path to go through, separated by a space.",
"inputOPTrigramsExample": "Ex: LSN CIG CCS",
"inverseOD": "Reverse origin/destination",
"launchSimulation": "Start",
"manageVias": "Steps management",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
"gare": "Gare, poste",
"importTrainSchedule": "Importation",
"infraLoading": "Infra en cours de chargement…",
"inputOPTrigrams": "Entrez les trigrammes des points remarquables par lesquels vous voulez que l'itinéraire se fasse, séparés par un espace.",
"inputOPTrigramsExample": "Ex : LSN CIG CCS",
"inverseOD": "Inverser origine/destination",
"launchSimulation": "Démarrer",
"manageVias": "Gestion des étapes",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import allowancesPic from 'assets/pictures/components/allowances.svg';
import simulationSettings from 'assets/pictures/components/simulationSettings.svg';
import RollingStock2Img from 'modules/rollingStock/components/RollingStock2Img';
import { isElectric } from 'modules/rollingStock/helpers/utils';
import { formatKmValue } from 'utils/strings';

export default function ManageTrainSchedule() {
const dispatch = useDispatch();
Expand Down Expand Up @@ -91,8 +92,7 @@ export default function ManageTrainSchedule() {
{t('tabs.pathFinding')}
{pathFinding?.length && !Number.isNaN(pathFinding.length) && (
<small className="ml-auto pl-1">
{Math.round(pathFinding.length) / 1000}
km
{pathFinding.length && formatKmValue(pathFinding.length / 1000, 3)}
</small>
)}
</span>
Expand Down
35 changes: 11 additions & 24 deletions front/src/common/Pathfinding/Pathfinding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@ import { Position } from 'geojson';
import bbox from '@turf/bbox';
import { useTranslation } from 'react-i18next';
import { compact, isEqual } from 'lodash';
import { BiCheckCircle, BiXCircle, BiErrorCircle } from 'react-icons/bi';
import { GoAlert, GoCheckCircle, GoStop } from 'react-icons/go';

import { setFailure } from 'reducers/main';

import { ArrayElement } from 'utils/types';
import { conditionalStringConcat, formatKmValue } from 'utils/strings';
import { lengthFromLineCoordinates } from 'utils/geometry';

import { Path, PathQuery, osrdEditoastApi } from 'common/api/osrdEditoastApi';
import { useModal } from 'common/BootstrapSNCF/ModalSNCF';
import { PointOnMap } from 'applications/operationalStudies/consts';

import {
Expand All @@ -31,8 +29,6 @@ import {
getPathfindingID,
getGeojson,
} from 'reducers/osrdconf/selectors';

import ModalPathJSONDetail from 'modules/trainschedule/components/ManageTrainSchedule/Itinerary/ModalPathJSONDetail';
import infraLogo from 'assets/pictures/components/tracks.svg';
import InfraLoadingState from 'applications/operationalStudies/components/Scenario/InfraLoadingState';
import { Spinner } from '../Loader';
Expand Down Expand Up @@ -265,7 +261,6 @@ function Pathfinding({ zoomToFeature }: PathfindingProps) {
const { t } = useTranslation(['operationalStudies/manageTrainSchedule']);
const [pathfindingRequest, setPathfindingRequest] =
useState<ReturnType<typeof postPathfinding>>();
const { openModal } = useModal();
const dispatch = useDispatch();
const infraID = useSelector(getInfraID, isEqual);
const origin = useSelector(getOrigin, isEqual);
Expand Down Expand Up @@ -345,7 +340,7 @@ function Pathfinding({ zoomToFeature }: PathfindingProps) {
const displayInfraSoftError = () => (
<div className="content pathfinding-error my-2">
<span className="lead">
<BiXCircle />
<GoStop />
</span>
{reloadCount <= 5 ? (
<span className="flex-grow-1">{t('errorMessages.unableToLoadInfra', { reloadCount })}</span>
Expand All @@ -358,7 +353,7 @@ function Pathfinding({ zoomToFeature }: PathfindingProps) {
const displayInfraHardError = () => (
<div className="content pathfinding-error my-2">
<span className="lead">
<BiXCircle />
<GoStop />
</span>
<span className="flex-grow-1">{t('errorMessages.hardErrorInfra')}</span>
</div>
Expand Down Expand Up @@ -468,23 +463,13 @@ function Pathfinding({ zoomToFeature }: PathfindingProps) {
params: {
origin,
destination,
vias,
rollingStockID,
},
});
}
}, [origin, destination, rollingStockID]);

const pathDetailsToggleButton = (
<button
type="button"
onClick={() => openModal(<ModalPathJSONDetail />, 'lg')}
className="btn btn-link details"
data-testid="result-pathfinding-distance"
>
{formatKmValue(lengthFromLineCoordinates(geojson?.geographic?.coordinates))}
</button>
);

const loaderPathfindingInProgress = (
<div className="pathfinding-in-progress">
<div className="pathfinding-in-progress-card">
Expand Down Expand Up @@ -519,7 +504,7 @@ function Pathfinding({ zoomToFeature }: PathfindingProps) {
useEffect(() => setIsPathfindingInitialized(true), []);

return (
<div className="pathfinding-state-main-container">
<div className="pathfinding-state-main-container flex-grow-1">
{infra && infra.state !== 'CACHED' && (
<div className="content infra-loading">
<img src={infraLogo} alt="Infra logo" className="mr-2" />
Expand All @@ -542,18 +527,20 @@ function Pathfinding({ zoomToFeature }: PathfindingProps) {
{pathfindingState.done && !pathfindingState.error && (
<div className="content pathfinding-done">
<span className="lead">
<BiCheckCircle />
<GoCheckCircle />
</span>
<span className="flex-grow-1">{t('pathfindingDone')}</span>
{pathDetailsToggleButton}
<small className="text-secondary">
{geojson?.length && formatKmValue(geojson?.length / 1000, 3)}
</small>
</div>
)}
{pathfindingState.error && (
<div
className={`content pathfinding-error ${infra && infra.state !== 'CACHED' && 'mt-2'}`}
>
<span className="lead">
<BiXCircle />
<GoStop />
</span>
<span className="flex-grow-1">
{t('pathfindingError', { errorMessage: t(pathfindingState.error) })}
Expand All @@ -563,7 +550,7 @@ function Pathfinding({ zoomToFeature }: PathfindingProps) {
{pathfindingState.missingParam && (
<div className="content missing-params">
<span className="lead">
<BiErrorCircle />
<GoAlert />
</span>
<span className="flex-grow-1">
{t('pathfindingMissingParams', { missingElements })}
Expand Down
180 changes: 180 additions & 0 deletions front/src/common/Pathfinding/TypeAndPath.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/* eslint-disable jsx-a11y/no-autofocus */
import {
Path,
PostSearchApiArg,
SearchResultItemOperationalPoint,
osrdEditoastApi,
} from 'common/api/osrdEditoastApi';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { getInfraID, getRollingStockID } from 'reducers/osrdconf/selectors';
import { useDebounce } from 'utils/helpers';
import cx from 'classnames';
import { GoAlert, GoTriangleRight } from 'react-icons/go';
import { loadPathFinding } from 'modules/trainschedule/components/ManageTrainSchedule/helpers/adjustConfWithTrainToModify';
import { setFailure } from 'reducers/main';
import bbox from '@turf/bbox';
import { Position } from 'geojson';

type SearchConstraintType = (string | number | string[])[];
type PathfindingProps = {
zoomToFeature: (lngLat: Position, id?: undefined, source?: undefined) => void;
};

const monospaceOneCharREMWidth = 0.6225;

function OpTooltips({ opList }: { opList: SearchResultItemOperationalPoint[] }) {
// Calculation of chars distance from left to put tooltip on center of op name
const calcLeftMargin = (charsFromLeft: number, length: number) =>
charsFromLeft * monospaceOneCharREMWidth + (length * monospaceOneCharREMWidth) / 2;
let charsFromLeft = 0;
return (
<div className="op-tooltips">
{opList.map((op, idx) => {
const leftMargin = calcLeftMargin(charsFromLeft, op.trigram.length);
charsFromLeft = charsFromLeft + op.trigram.length + 1;
return (
op.trigram !== '' && (
<div
className={cx('op', { wrong: !op.name })}
key={`typeandpath-op-${idx}-${op.trigram}`}
style={{ left: `${leftMargin}rem` }}
>
{op.name ? op.name : <GoAlert />}
</div>
)
);
})}
</div>
);
}

export default function TypeAndPath({ zoomToFeature }: PathfindingProps) {
const dispatch = useDispatch();
const [inputText, setInputText] = useState('');
const [opList, setOpList] = useState<SearchResultItemOperationalPoint[]>([]);
const infraId = useSelector(getInfraID);
const rollingStockId = useSelector(getRollingStockID);
const [postSearch] = osrdEditoastApi.endpoints.postSearch.useMutation();
const [postPathfinding] = osrdEditoastApi.endpoints.postPathfinding.useMutation();
const { t } = useTranslation('operationalStudies/manageTrainSchedule');

const debouncedInputText = useDebounce(inputText.trimEnd(), 500);

const handleInput = (text: string) => {
setInputText(text.trimStart());
};

function getOpNames() {
if (infraId !== undefined) {
const opTrigrams = inputText.toUpperCase().trimEnd().split(' ');
const constraint = opTrigrams.reduce(
(res, trigram) => [...res, ['=', ['trigram'], trigram]],
['or'] as (string | SearchConstraintType)[]
);
// SNCF trigrams come with a yard name, for main station it could be nothing '',
// 'BV' (as Bâtiment Voyageurs) or '00', all are the same signification: this is the main station.
const limitToMainStationConstraint = [
'or',
['=', ['ch'], ''],
['=', ['ch'], 'BV'],
['=', ['ch'], '00'],
];
const payload: PostSearchApiArg = {
searchPayload: {
object: 'operationalpoint',
query: ['and', constraint, ['=', ['infra_id'], infraId], limitToMainStationConstraint],
},
pageSize: 1000,
};
postSearch(payload)
.unwrap()
.then((results) => {
const operationalPoints = [...results] as SearchResultItemOperationalPoint[];
setOpList(
opTrigrams.map(
(trigram) => operationalPoints.find((op) => op.trigram === trigram) || { trigram }
) as SearchResultItemOperationalPoint[]
);
});
}
}

const isInvalid = useMemo(() => opList.some((op) => !op.name && op.trigram !== ''), [opList]);

function launchPathFinding() {
if (infraId && rollingStockId && opList.length > 0) {
const params = {
infra: infraId,
rolling_stocks: [rollingStockId],
steps: opList
.filter((op) => op.trigram !== '')
.map((op) => ({
duration: 0,
waypoints: op.track_sections.map((position) => ({
track_section: position.track,
offset: position.position,
})),
})),
};
postPathfinding({ pathQuery: params })
.unwrap()
.then((itineraryCreated: Path) => {
zoomToFeature(bbox(itineraryCreated.geographic));
loadPathFinding(itineraryCreated, dispatch);
})
.catch((e) => {
dispatch(
setFailure({
name: e.data.name,
message: e.data.message,
})
);
});
}
}

useEffect(() => {
if (debouncedInputText !== '') {
getOpNames();
} else {
setOpList([]);
}
}, [debouncedInputText]);

return (
<div
className="type-and-path"
style={{ minWidth: `${monospaceOneCharREMWidth * inputText.length + 5.5}rem` }} // To grow input field & whole div along text size
>
<div className="help">{opList.length === 0 && t('inputOPTrigrams')}</div>
<OpTooltips opList={opList} />
<div className="d-flex align-items-center">
<div
className={cx('form-control-container', 'flex-grow-1', 'mr-2', {
'is-invalid': isInvalid,
})}
>
<input
className="form-control form-control-sm text-zone"
type="text"
value={inputText}
onChange={(e) => handleInput(e.target.value)}
placeholder={t('inputOPTrigramsExample')}
autoFocus
/>
<span className="form-control-state" />
</div>
<button
className="btn btn-sm btn-success"
type="button"
onClick={launchPathFinding}
disabled={isInvalid || opList.length < 2}
>
<GoTriangleRight />
</button>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function Origin(props: OriginProps) {
<div className="mb-2 place" data-testid="itinerary-origin">
{origin !== undefined ? (
<>
<div className="hover origin-name-and-time-container">
<div className="pl-1 hover w-100 d-flex align-items-center">
<span className="text-success mr-2">
<RiMapPin2Fill />
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default function DisplayVias({ zoomToFeaturePoint }: DisplayViasProps) {
<div {...provided.droppableProps} ref={provided.innerRef}>
{osrdconf.vias.map((place, index) => (
<Draggable
key={`drag-key-${place.id}-${place.position}`}
key={`drag-key-${place.id}-${place.path_offset}`}
draggableId={`drag-vias-${index}`}
index={index}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import ModalSugerredVias from 'modules/trainschedule/components/ManageTrainSched
import { getOrigin, getDestination, getVias } from 'reducers/osrdconf/selectors';
import { getMap } from 'reducers/map/selectors';
import Pathfinding from 'common/Pathfinding/Pathfinding';
import TypeAndPath from 'common/Pathfinding/TypeAndPath';
import { GoRocket } from 'react-icons/go';

function Itinerary() {
const origin = useSelector(getOrigin);
const destination = useSelector(getDestination);
const vias = useSelector(getVias);
const [extViewport, setExtViewport] = useState<Viewport>();
const [displayTypeAndPath, setDisplayTypeAndPath] = useState(false);
const dispatch = useDispatch();
const map = useSelector(getMap);

Expand Down Expand Up @@ -88,10 +91,22 @@ function Itinerary() {
}, [extViewport]);

return (
<div className="itinerary mb-2">
<div className="mb-2">
<div className="osrd-config-item">
<div className="mb-2 d-flex">
<Pathfinding zoomToFeature={zoomToFeature} />
<button
type="button"
className="btn btn-sm btn-only-icon btn-white px-3 ml-2"
onClick={() => setDisplayTypeAndPath(!displayTypeAndPath)}
>
<GoRocket />
</button>
</div>
{displayTypeAndPath && (
<div className="mb-2">
<TypeAndPath zoomToFeature={zoomToFeature} />
</div>
)}
<div className="osrd-config-item-container pathfinding-details" data-testid="itinerary">
<DisplayItinerary
zoomToFeaturePoint={zoomToFeaturePoint}
Expand Down
Loading