Skip to content

Commit

Permalink
front: introduce BaseMap
Browse files Browse the repository at this point in the history
Signed-off-by: Clara Ni <[email protected]>
  • Loading branch information
clarani committed Feb 7, 2025
1 parent e874847 commit b37e653
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 370 deletions.
122 changes: 26 additions & 96 deletions front/src/applications/referenceMap/Map.tsx
Original file line number Diff line number Diff line change
@@ -1,142 +1,72 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useMemo, useRef } from 'react';

import { isNil } from 'lodash';
import ReactMapGL, { AttributionControl, ScaleControl } from 'react-map-gl/maplibre';
import type { MapRef } from 'react-map-gl/maplibre';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';

import BaseMap from 'common/Map/BaseMap';
import MapButtons from 'common/Map/Buttons/MapButtons';
import { CUSTOM_ATTRIBUTION } from 'common/Map/const';
import colors from 'common/Map/Consts/colors';
import { useMapBlankStyle } from 'common/Map/Layers/blankStyle';
import IGNLayers from 'common/Map/Layers/IGNLayers';
import InfraObjectLayers from 'common/Map/Layers/InfraObjectLayers';
import LineSearchLayer from 'common/Map/Layers/LineSearchLayer';
import OSMLayers from 'common/Map/Layers/OSMLayers';
import SearchMarker from 'common/Map/Layers/SearchMarker';
import { removeSearchItemMarkersOnMap } from 'common/Map/utils';
import { useInfraID } from 'common/osrdContext';
import { LAYER_GROUPS_ORDER, LAYERS } from 'config/layerOrder';
import VirtualLayers from 'modules/simulationResult/components/SimulationResultsMap/VirtualLayers';
import type { Viewport } from 'reducers/map';
import { updateViewport } from 'reducers/map';
import { getMap, getTerrain3DExaggeration } from 'reducers/map/selectors';
import { useAppDispatch } from 'store';

function Map() {
const mapBlankStyle = useMapBlankStyle();

const [mapLoaded, setMapLoaded] = useState(false);
const Map = () => {
const dispatch = useAppDispatch();
const { viewport, mapSearchMarker, mapStyle, showOSM, layersSettings } = useSelector(getMap);
const infraID = useInfraID();
const terrain3DExaggeration = useSelector(getTerrain3DExaggeration);

const mapRef = useRef<MapRef | null>(null);
const { urlLat, urlLon, urlZoom, urlBearing, urlPitch } = useParams();
const dispatch = useAppDispatch();

const updateViewportChange = useCallback(
(value: Partial<Viewport>, updateRouter = false) => {
(value: Partial<Viewport>, { updateRouter } = { updateRouter: false }) => {
dispatch(updateViewport(value, `/map`, updateRouter));
},
[dispatch]
);

const scaleControlStyle = {
left: 20,
bottom: 20,
};

const resetPitchBearing = () => {
updateViewportChange({
...viewport,
bearing: 0,
pitch: 0,
});
};

const defineInteractiveLayers = () => {
const interactiveLayersLocal = [];
if (layersSettings.tvds) {
interactiveLayersLocal.push('chartis/osrd_tvd_section/geo');
}
return interactiveLayersLocal;
};

/**
* When the component mount
* => we check if url has viewport and set it in store
*/
useEffect(() => {
// viewport
const newViewport: Partial<Viewport> = {};
if (!isNil(urlLat)) newViewport.latitude = parseFloat(urlLat);
if (!isNil(urlLon)) newViewport.longitude = parseFloat(urlLon);
if (!isNil(urlZoom)) newViewport.zoom = parseFloat(urlZoom);
if (!isNil(urlBearing)) newViewport.bearing = parseFloat(urlBearing);
if (!isNil(urlPitch)) newViewport.pitch = parseFloat(urlPitch);
if (Object.keys(newViewport).length > 0) updateViewportChange(newViewport);
// we only do it at mount time
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const interactiveLayerIds = useMemo(
() => (layersSettings.tvds ? ['chartis/osrd_tvd_section/geo'] : []),
[layersSettings]
);

return (
<main className="mastcontainer mastcontainer-map">
<MapButtons
map={mapRef.current ?? undefined}
resetPitchBearing={resetPitchBearing}
withInfraButton
withMapKeyButton
bearing={viewport.bearing}
viewPort={viewport}
withInfraButton
withMapKeyButton
/>
<ReactMapGL
{...viewport}
ref={mapRef}
<BaseMap
mapId="reference-map"
mapRef={mapRef}
cursor="normal"
style={{ width: '100%', height: '100%' }}
mapStyle={mapBlankStyle}
onMove={(e) => updateViewportChange(e.viewState)}
onMoveEnd={(e) => updateViewportChange(e.viewState, true)}
attributionControl={false} // Defined below
onResize={(e) => {
updateViewportChange({
width: e.target.getContainer().offsetWidth,
height: e.target.getContainer().offsetHeight,
});
}}
interactiveLayerIds={defineInteractiveLayers()}
touchZoomRotate
maxPitch={85}
terrain={
terrain3DExaggeration
? { source: 'terrain', exaggeration: terrain3DExaggeration }
: undefined
}
onLoad={() => {
setMapLoaded(true);
}}
infraId={infraID}
interactiveLayerIds={interactiveLayerIds}
mapSearchMarker={mapSearchMarker}
mapStyle={mapStyle}
onClick={() => {
removeSearchItemMarkersOnMap(dispatch);
}}
>
<VirtualLayers />
<AttributionControl customAttribution={CUSTOM_ATTRIBUTION} />
<ScaleControl maxWidth={100} unit="metric" style={scaleControlStyle} />

<OSMLayers mapStyle={mapStyle} showOSM={showOSM && mapLoaded} />
<IGNLayers />

{infraID && <InfraObjectLayers infraId={infraID} mapStyle={mapStyle} />}

<LineSearchLayer
layerOrder={LAYER_GROUPS_ORDER[LAYERS.LINE_SEARCH.GROUP]}
infraID={infraID}
/>

{mapSearchMarker && <SearchMarker data={mapSearchMarker} colors={colors[mapStyle]} />}
</ReactMapGL>
showOSM={showOSM}
viewPort={viewport}
updatePartialViewPort={updateViewportChange}
terrain3DExaggeration={terrain3DExaggeration}
/>
</main>
);
}
};

export default Map;
155 changes: 155 additions & 0 deletions front/src/common/Map/BaseMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { type MutableRefObject, type PropsWithChildren, useEffect, useState } from 'react';

import type { MapLayerMouseEvent, MapLibreEvent } from 'maplibre-gl';
import ReactMapGL, { AttributionControl, ScaleControl } from 'react-map-gl/maplibre';
import type { MapRef } from 'react-map-gl/maplibre';
import { useParams } from 'react-router-dom';

import colors from 'common/Map/Consts/colors';
import { useMapBlankStyle } from 'common/Map/Layers/blankStyle';
import IGNLayers from 'common/Map/Layers/IGNLayers';
import InfraObjectLayers from 'common/Map/Layers/InfraObjectLayers';
import LineSearchLayer from 'common/Map/Layers/LineSearchLayer';
import OSMLayers from 'common/Map/Layers/OSMLayers';
import SearchMarker from 'common/Map/Layers/SearchMarker';
import { LAYER_GROUPS_ORDER, LAYERS } from 'config/layerOrder';
import VirtualLayers from 'modules/simulationResult/components/SimulationResultsMap/VirtualLayers';
import type { MapState, Viewport } from 'reducers/map';

import { CUSTOM_ATTRIBUTION } from './const';

type MapProps = Pick<MapState, 'mapSearchMarker' | 'mapStyle' | 'showOSM'> & {
mapId: string;
mapRef: MutableRefObject<MapRef | null>;
interactiveLayerIds: string[];
infraId?: number;
terrain3DExaggeration?: number;
viewPort: Viewport;
updatePartialViewPort: (
newPartialViewPort: Partial<Viewport>,
options?: { updateRouter: boolean }
) => void;
cursor?: 'default' | 'pointer' | 'normal';
hideAttribution?: boolean;
hoveredOperationalPointId?: string;
onClick?: ((e: MapLayerMouseEvent) => void) | undefined;
onMouseEnter?: ((e: MapLayerMouseEvent) => void) | undefined;
onMouseMove?: ((e: MapLayerMouseEvent) => void) | undefined;
onIdle?: ((e: MapLibreEvent) => void) | undefined;
};

const BaseMap = ({
mapId,
mapRef,
children,
interactiveLayerIds,
viewPort,
infraId,
mapSearchMarker,
mapStyle,
showOSM,
cursor = 'default',
hideAttribution = false,
hoveredOperationalPointId,
terrain3DExaggeration,
updatePartialViewPort,
onClick,
onMouseEnter,
onMouseMove,
onIdle,
}: PropsWithChildren<MapProps>) => {
const mapBlankStyle = useMapBlankStyle();

const [mapIsLoaded, setMapIsLoaded] = useState(false);

const { urlLat = '', urlLon = '', urlZoom = '', urlBearing = '', urlPitch = '' } = useParams();

useEffect(() => {
if (urlLat) {
updatePartialViewPort({
latitude: parseFloat(urlLat),
longitude: parseFloat(urlLon),
zoom: parseFloat(urlZoom),
bearing: parseFloat(urlBearing),
pitch: parseFloat(urlPitch),
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<ReactMapGL
id={mapId}
ref={mapRef}
{...viewPort}
interactiveLayerIds={interactiveLayerIds}
cursor={cursor}
mapStyle={mapBlankStyle}
terrain={
terrain3DExaggeration
? { source: 'terrain', exaggeration: terrain3DExaggeration }
: undefined
}
onMouseEnter={onMouseEnter}
onMouseMove={onMouseMove}
onClick={onClick}
onIdle={onIdle}
// default behavior
onMove={(e) => {
updatePartialViewPort(e.viewState);
}}
onMoveEnd={(e) => updatePartialViewPort(e.viewState, { updateRouter: true })}
onResize={(e) => {
updatePartialViewPort({
width: e.target.getContainer().offsetWidth,
height: e.target.getContainer().offsetHeight,
});
}}
onLoad={() => {
setMapIsLoaded(true);
}}
attributionControl={false} // Defined below
dragPan
maxPitch={85}
preserveDrawingBuffer
scrollZoom
style={{ width: '100%', height: '100%' }}
touchZoomRotate
>
<VirtualLayers />
{!hideAttribution && (
<AttributionControl position="bottom-right" customAttribution={CUSTOM_ATTRIBUTION} />
)}
<ScaleControl
maxWidth={100}
unit="metric"
style={{
left: 20,
bottom: 20,
}}
/>

{infraId && (
<InfraObjectLayers
infraId={infraId}
mapStyle={mapStyle}
hoveredOperationalPointId={hoveredOperationalPointId}
/>
)}

<OSMLayers mapStyle={mapStyle} showOSM={showOSM && mapIsLoaded} />
<IGNLayers />

<LineSearchLayer
layerOrder={LAYER_GROUPS_ORDER[LAYERS.LINE_SEARCH.GROUP]}
infraID={infraId}
/>

{mapSearchMarker && <SearchMarker data={mapSearchMarker} colors={colors[mapStyle]} />}

{children}
</ReactMapGL>
);
};

export default BaseMap;
Loading

0 comments on commit b37e653

Please sign in to comment.