From f238c148c158e2d8cde48d7cafaf5da3c9c7aa44 Mon Sep 17 00:00:00 2001 From: Florian Amsallem Date: Fri, 7 Aug 2026 18:45:21 +0200 Subject: [PATCH 1/2] front: add bbox fetching and viewport adjustment Integrated for Editor, ItineraryModalMap, and Map components. Signed-off-by: Florian Amsallem --- front/src/applications/editor/Editor.tsx | 35 +++++++++++++- .../Itinerary/ItineraryModalMap.tsx | 43 +++++++++++++++++ front/src/applications/referenceMap/Map.tsx | 47 ++++++++++++++++++- 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/front/src/applications/editor/Editor.tsx b/front/src/applications/editor/Editor.tsx index 7736ed70ca1..a43e586067d 100644 --- a/front/src/applications/editor/Editor.tsx +++ b/front/src/applications/editor/Editor.tsx @@ -20,7 +20,7 @@ import useSwitchTypes from 'applications/editor/tools/switchEdition/useSwitchTyp import type { switchProps } from 'applications/editor/tools/switchProps'; import type { CommonToolState } from 'applications/editor/tools/types'; import { centerMapOnObject, selectEntities } from 'applications/editor/tools/utils'; -import type { ObjectType } from 'common/api/osrdEditoastApi'; +import { osrdEditoastApi, type ObjectType } from 'common/api/osrdEditoastApi'; import useCheckUserPrivileges from 'common/authorization/hooks/useCheckUserPrivileges'; import useProtectedAction from 'common/authorization/hooks/useProtectedAction'; import { useModal } from 'common/BootstrapSNCF/ModalSNCF'; @@ -28,6 +28,7 @@ import { LoaderState } from 'common/Loaders'; import MapButtons from 'common/Map/Buttons/MapButtons'; import MapSearch from 'common/Map/Search/MapSearch'; import { MapContextProvider } from 'common/Map/useMapContext'; +import { computeBBoxViewport } from 'common/Map/WarpedMap/core/helpers'; import { useInfraActions, useInfraID, useOsrdActions } from 'common/osrdContext'; import useInfra from 'modules/infra/useInfra'; import { useMapSettings, useMapSettingsActions } from 'reducers/commonMap'; @@ -57,8 +58,10 @@ const Editor = () => { useMapSettingsActions(); const mapRef = useRef(null); + const focusedInfraIdRef = useRef(undefined); const { urlInfra } = useParams(); const infraID = useInfraID(); + const [getInfraByInfraIdBbox] = osrdEditoastApi.endpoints.getInfraByInfraIdBbox.useLazyQuery(); const [searchParams, setSearchParams] = useSearchParams(); const isLoading = useSelector(getIsLoading); const isLocked = useSelector(getInfraLockStatus); @@ -310,6 +313,36 @@ const Editor = () => { } }, [urlInfra]); + useEffect(() => { + if (isNil(infraID) || focusedInfraIdRef.current === infraID) { + return; + } + + focusedInfraIdRef.current = infraID; + + void getInfraByInfraIdBbox({ infraId: infraID }) + .unwrap() + .then((infraBbox) => { + if (!infraBbox) { + return; + } + + const mapContainer = mapRef.current?.getContainer(); + const newViewport = computeBBoxViewport( + [infraBbox.min_lon, infraBbox.min_lat, infraBbox.max_lon, infraBbox.max_lat], + viewport, + { + width: mapContainer?.clientWidth, + height: mapContainer?.clientHeight, + padding: 100, + } + ); + + setViewport(newViewport); + }) + .catch(() => undefined); + }, [infraID, getInfraByInfraIdBbox, setViewport, viewport]); + // Lifecycle events on tools: useEffect(() => { if (toolAndState.tool.onMount) toolAndState.tool.onMount(extendedContext); diff --git a/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/ItineraryModalMap.tsx b/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/ItineraryModalMap.tsx index 9496a1da1f5..fa28d58e773 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/ItineraryModalMap.tsx +++ b/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/ItineraryModalMap.tsx @@ -16,6 +16,7 @@ import MapButtons from 'common/Map/Buttons/MapButtons'; import PathStepMarker, { PATH_STEP_MARKER_STATE } from 'common/Map/components/PathStepMarker'; import { SnappedMarker } from 'common/Map/Layers'; import { MapContextProvider } from 'common/Map/useMapContext'; +import { computeBBoxViewport } from 'common/Map/WarpedMap/core/helpers'; import { useInfraID } from 'common/osrdContext'; import { LAYER_GROUPS_ORDER, LAYERS } from 'config/layerOrder'; import Itinerary from 'modules/simulationResult/components/SimulationResultsMap/RenderItinerary'; @@ -81,6 +82,7 @@ const ItineraryModalMap = ({ } = useMapSettingsActions(); const mapRef = useRef(null); + const focusedInfraIdRef = useRef(undefined); const [hoveredOperationalPointId, setHoveredOperationalPointId] = useState(); const [isDraggingMarker, setIsDraggingMarker] = useState(false); @@ -98,11 +100,16 @@ const ItineraryModalMap = ({ const [getInfraObjectEntity] = osrdEditoastApi.endpoints.postInfraByInfraIdObjectsAndObjectType.useLazyQuery(); + const [getInfraByInfraIdBbox] = osrdEditoastApi.endpoints.getInfraByInfraIdBbox.useLazyQuery(); const lastStepHasLocation = !!pathSteps?.at(-1)?.location; const lastRealStepIndex = pathSteps ? pathSteps.length - (lastStepHasLocation ? 1 : 2) : -1; const isNewPlacement = isMapSelectionMode; + const locatedStepsCount = useMemo( + () => pathSteps?.filter((step) => step.location !== null).length ?? 0, + [pathSteps] + ); useEffect(() => { if (!isMapSelectionMode) { @@ -314,6 +321,42 @@ const ItineraryModalMap = ({ return result; }, [layersSettings, isMapSelectionMode]); + useEffect(() => { + if (pathProperties?.geometry) { + focusedInfraIdRef.current = undefined; + return; + } + + // Initial state: focus on infra until a valid itinerary can drive map framing. + if (!infraID || locatedStepsCount >= 2 || focusedInfraIdRef.current === infraID) { + return; + } + + focusedInfraIdRef.current = infraID; + + void getInfraByInfraIdBbox({ infraId: infraID }) + .unwrap() + .then((infraBbox) => { + if (!infraBbox) { + return; + } + + const mapContainer = mapRef.current?.getContainer(); + const newViewport = computeBBoxViewport( + [infraBbox.min_lon, infraBbox.min_lat, infraBbox.max_lon, infraBbox.max_lat], + viewport, + { + width: mapContainer?.clientWidth, + height: mapContainer?.clientHeight, + padding: 100, + } + ); + + dispatch(updateViewport(newViewport)); + }) + .catch(() => undefined); + }, [infraID, locatedStepsCount, pathProperties?.geometry, getInfraByInfraIdBbox, viewport]); + return ( { useMapSettingsActions(); const infraID = useInfraID(); + const { urlLat, urlLon, urlZoom, urlBearing, urlPitch } = useParams(); const mapRef = useRef(null); + const focusedInfraIdRef = useRef(undefined); + const skipNextInfraAutoFocusRef = useRef( + Boolean(urlLat && urlLon && urlZoom && urlBearing && urlPitch) + ); + const [getInfraByInfraIdBbox] = osrdEditoastApi.endpoints.getInfraByInfraIdBbox.useLazyQuery(); const updateMapSettings = useCallback( (value: Partial) => { @@ -58,6 +67,42 @@ const Map = () => { [layersSettings.track_sections] ); + useEffect(() => { + if (!infraID || focusedInfraIdRef.current === infraID) { + return; + } + + if (skipNextInfraAutoFocusRef.current) { + skipNextInfraAutoFocusRef.current = false; + focusedInfraIdRef.current = infraID; + return; + } + + focusedInfraIdRef.current = infraID; + + void getInfraByInfraIdBbox({ infraId: infraID }) + .unwrap() + .then((infraBbox) => { + if (!infraBbox) { + return; + } + + const mapContainer = mapRef.current?.getContainer(); + const newViewport = computeBBoxViewport( + [infraBbox.min_lon, infraBbox.min_lat, infraBbox.max_lon, infraBbox.max_lat], + viewport, + { + width: mapContainer?.clientWidth, + height: mapContainer?.clientHeight, + padding: 100, + } + ); + + updateViewportChange(newViewport); + }) + .catch(() => undefined); + }, [infraID, getInfraByInfraIdBbox, updateViewportChange, viewport]); + return (
Date: Fri, 7 Aug 2026 20:27:47 +0200 Subject: [PATCH 2/2] fixup! front: add bbox fetching and viewport adjustment --- front/src/applications/editor/Editor.tsx | 41 ++++---------- .../Itinerary/ItineraryModalMap.tsx | 54 ++++++------------ front/src/applications/referenceMap/Map.tsx | 56 ++++++------------- 3 files changed, 46 insertions(+), 105 deletions(-) diff --git a/front/src/applications/editor/Editor.tsx b/front/src/applications/editor/Editor.tsx index a43e586067d..a2165cf4ea7 100644 --- a/front/src/applications/editor/Editor.tsx +++ b/front/src/applications/editor/Editor.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ChevronLeft, ChevronRight } from '@osrd-project/ui-icons'; +import { skipToken } from '@reduxjs/toolkit/query'; import cx from 'classnames'; import { isNil, toInteger } from 'lodash'; import { useTranslation } from 'react-i18next'; @@ -58,10 +59,8 @@ const Editor = () => { useMapSettingsActions(); const mapRef = useRef(null); - const focusedInfraIdRef = useRef(undefined); const { urlInfra } = useParams(); const infraID = useInfraID(); - const [getInfraByInfraIdBbox] = osrdEditoastApi.endpoints.getInfraByInfraIdBbox.useLazyQuery(); const [searchParams, setSearchParams] = useSearchParams(); const isLoading = useSelector(getIsLoading); const isLocked = useSelector(getInfraLockStatus); @@ -313,35 +312,19 @@ const Editor = () => { } }, [urlInfra]); - useEffect(() => { - if (isNil(infraID) || focusedInfraIdRef.current === infraID) { - return; - } - - focusedInfraIdRef.current = infraID; - - void getInfraByInfraIdBbox({ infraId: infraID }) - .unwrap() - .then((infraBbox) => { - if (!infraBbox) { - return; - } + const { data: infraBbox } = osrdEditoastApi.endpoints.getInfraByInfraIdBbox.useQuery( + infraID ? { infraId: infraID } : skipToken + ); + useMemo(() => { + if (infraBbox === undefined) return; + const { min_lat, min_lon, max_lat, max_lon } = infraBbox!; - const mapContainer = mapRef.current?.getContainer(); - const newViewport = computeBBoxViewport( - [infraBbox.min_lon, infraBbox.min_lat, infraBbox.max_lon, infraBbox.max_lat], - viewport, - { - width: mapContainer?.clientWidth, - height: mapContainer?.clientHeight, - padding: 100, - } - ); + const newViewport = computeBBoxViewport([min_lon, min_lat, max_lon, max_lat], viewport, { + padding: 64, + }); - setViewport(newViewport); - }) - .catch(() => undefined); - }, [infraID, getInfraByInfraIdBbox, setViewport, viewport]); + setViewport(newViewport); + }, [infraBbox]); // Lifecycle events on tools: useEffect(() => { diff --git a/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/ItineraryModalMap.tsx b/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/ItineraryModalMap.tsx index fa28d58e773..2161315180e 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/ItineraryModalMap.tsx +++ b/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/ItineraryModalMap.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState, type PropsWithChildren } from 'react'; +import { skipToken } from '@reduxjs/toolkit/query'; import type { Feature, Position } from 'geojson'; import { useTranslation } from 'react-i18next'; import type { MapLayerMouseEvent, MapRef } from 'react-map-gl/maplibre'; @@ -82,7 +83,6 @@ const ItineraryModalMap = ({ } = useMapSettingsActions(); const mapRef = useRef(null); - const focusedInfraIdRef = useRef(undefined); const [hoveredOperationalPointId, setHoveredOperationalPointId] = useState(); const [isDraggingMarker, setIsDraggingMarker] = useState(false); @@ -100,7 +100,6 @@ const ItineraryModalMap = ({ const [getInfraObjectEntity] = osrdEditoastApi.endpoints.postInfraByInfraIdObjectsAndObjectType.useLazyQuery(); - const [getInfraByInfraIdBbox] = osrdEditoastApi.endpoints.getInfraByInfraIdBbox.useLazyQuery(); const lastStepHasLocation = !!pathSteps?.at(-1)?.location; const lastRealStepIndex = pathSteps ? pathSteps.length - (lastStepHasLocation ? 1 : 2) : -1; @@ -111,6 +110,21 @@ const ItineraryModalMap = ({ [pathSteps] ); + const { data: infraBbox } = osrdEditoastApi.endpoints.getInfraByInfraIdBbox.useQuery( + infraID ? { infraId: infraID } : skipToken + ); + + useMemo(() => { + if (infraBbox === undefined) return; + if (locatedStepsCount >= 2) return; + const { min_lat, min_lon, max_lat, max_lon } = infraBbox!; + + const newViewport = computeBBoxViewport([min_lon, min_lat, max_lon, max_lat], viewport, { + padding: 64, + }); + updateViewport(newViewport); + }, [infraBbox, locatedStepsCount]); + useEffect(() => { if (!isMapSelectionMode) { setCursorMarker(null); @@ -321,42 +335,6 @@ const ItineraryModalMap = ({ return result; }, [layersSettings, isMapSelectionMode]); - useEffect(() => { - if (pathProperties?.geometry) { - focusedInfraIdRef.current = undefined; - return; - } - - // Initial state: focus on infra until a valid itinerary can drive map framing. - if (!infraID || locatedStepsCount >= 2 || focusedInfraIdRef.current === infraID) { - return; - } - - focusedInfraIdRef.current = infraID; - - void getInfraByInfraIdBbox({ infraId: infraID }) - .unwrap() - .then((infraBbox) => { - if (!infraBbox) { - return; - } - - const mapContainer = mapRef.current?.getContainer(); - const newViewport = computeBBoxViewport( - [infraBbox.min_lon, infraBbox.min_lat, infraBbox.max_lon, infraBbox.max_lat], - viewport, - { - width: mapContainer?.clientWidth, - height: mapContainer?.clientHeight, - padding: 100, - } - ); - - dispatch(updateViewport(newViewport)); - }) - .catch(() => undefined); - }, [infraID, locatedStepsCount, pathProperties?.geometry, getInfraByInfraIdBbox, viewport]); - return ( { useMapSettingsActions(); const infraID = useInfraID(); - const { urlLat, urlLon, urlZoom, urlBearing, urlPitch } = useParams(); const mapRef = useRef(null); - const focusedInfraIdRef = useRef(undefined); - const skipNextInfraAutoFocusRef = useRef( - Boolean(urlLat && urlLon && urlZoom && urlBearing && urlPitch) - ); - const [getInfraByInfraIdBbox] = osrdEditoastApi.endpoints.getInfraByInfraIdBbox.useLazyQuery(); const updateMapSettings = useCallback( (value: Partial) => { @@ -67,41 +62,26 @@ const Map = () => { [layersSettings.track_sections] ); - useEffect(() => { - if (!infraID || focusedInfraIdRef.current === infraID) { - return; - } - + const { urlLat } = useParams(); + const skipNextInfraAutoFocusRef = useRef(urlLat !== undefined); + const { data: infraBbox } = osrdEditoastApi.endpoints.getInfraByInfraIdBbox.useQuery( + infraID ? { infraId: infraID } : skipToken + ); + useMemo(() => { + if (infraBbox === undefined) return; if (skipNextInfraAutoFocusRef.current) { skipNextInfraAutoFocusRef.current = false; - focusedInfraIdRef.current = infraID; return; } - focusedInfraIdRef.current = infraID; - - void getInfraByInfraIdBbox({ infraId: infraID }) - .unwrap() - .then((infraBbox) => { - if (!infraBbox) { - return; - } - - const mapContainer = mapRef.current?.getContainer(); - const newViewport = computeBBoxViewport( - [infraBbox.min_lon, infraBbox.min_lat, infraBbox.max_lon, infraBbox.max_lat], - viewport, - { - width: mapContainer?.clientWidth, - height: mapContainer?.clientHeight, - padding: 100, - } - ); - - updateViewportChange(newViewport); - }) - .catch(() => undefined); - }, [infraID, getInfraByInfraIdBbox, updateViewportChange, viewport]); + const { min_lat, min_lon, max_lat, max_lon } = infraBbox!; + + const newViewport = computeBBoxViewport([min_lon, min_lat, max_lon, max_lat], viewport, { + padding: 64, + }); + + updateViewportChange(newViewport); + }, [infraBbox]); return (