diff --git a/ui-speedspacechart/README.md b/ui-speedspacechart/README.md index 7b847d30..ce0ae8f3 100644 --- a/ui-speedspacechart/README.md +++ b/ui-speedspacechart/README.md @@ -1,12 +1,16 @@ # ui-speedspacechart -The `ui-speedspacechart` package is part of the OSRD project, providing a specialized chart component designed to visualize speed and space data in a dynamic and interactive way. It leverages modern web technologies to offer a rich user experience for data analysis and presentation. +The `ui-speedspacechart` package is part of the OSRD project, providing a specialized chart +component designed to visualize speed and space data in a dynamic and interactive way. It leverages +modern web technologies to offer a rich user experience for data analysis and presentation. ## Features - **Dynamic Visualization**: Offers a comprehensive view of speed and space data over time. -- **Interactive Controls**: Users can interact with the chart to explore different aspects of the data. -- **Customizable Appearance**: Supports theming and customization to match different UI requirements. +- **Interactive Controls**: Users can interact with the chart to explore different aspects of the + data. +- **Customizable Appearance**: Supports theming and customization to match different UI + requirements. - **High Performance**: Optimized for performance, even with large datasets. ## Installation @@ -41,57 +45,64 @@ export default App; ## Types -| Field Name | Type | Description | -|-|-|-| -| `speeds` | `LayerData[]` | Array with numerical values representing speeds. | -| `ecoSpeeds` | `LayerData[]` | Array with numerical values representing eco speeds. | -| `stops` | `LayerData[]` | Array with string values representing stops. | -| `electrifications` | `LayerData[]` | Array with electrification values. | -| `slopes` | `LayerData[]` | Array with numerical values representing slopes. | -| `electricalProfiles` | `LayerData[]` (optional) | Optional array with electrical profile values. | -| `powerRestrictions` | `LayerData[]` (optional) | Optional array with power restriction values. | -| `speedLimitTags` | `LayerData[]` (optional) | Optional array with speed limit tag values. | - -LayerData is a generic type that encapsulates a layer's data, where T is the type of the value contained in the layer. It is defined as follows: - -| Field Name | Type | Description | -|-|-|-| +| Field Name | Type | Description | +| -------------------- | ------------------------------------------------- | ------------------------------------------------------------------------- | +| `speeds` | `LayerData[]` | Array with numerical values representing speeds. | +| `ecoSpeeds` | `LayerData[]` | Array with numerical values representing eco speeds. | +| `stops` | `LayerData[]` | Array with string values representing stops. | +| `electrifications` | `LayerData[]` | Array with electrification values. | +| `slopes` | `LayerData[]` | Array with numerical values representing slopes. | +| `trainLength` | `number` | The train length in meters. | +| `electricalProfiles` | `LayerData[]` (optional) | Optional array with electrical profile values. | +| `powerRestrictions` | `LayerData[]` (optional) | Optional array with power restriction values. | +| `speedLimitTags` | `LayerData[]` (optional) | Optional array with speed limit tag values. | +| `mrsp` | `ValuesAlongPath[]` (optional) | Optional struct with most restricted speed profile values along the path. | + +LayerData is a generic type that encapsulates a layer's data, where T is the type of the value +contained in the layer. It is defined as follows: + +| Field Name | Type | Description | +| ---------- | -------- | ------------------------------------------------------------------------------------- | | `position` | `Object` | Object containing start and optionally end numbers representing the layer's position. | -| `value` | `T` | The value of the layer, where T can be any of the specific types mentioned above. | +| `value` | `T` | The value of the layer, where T can be any of the specific types mentioned above. | Specific types for LayerData values: - `PowerRestrictionValues` - - `powerRestriction`: string - - `handled`: boolean + - `powerRestriction`: string + - `handled`: boolean - `ElectricalPofilelValues` - - `electricalProfile`: string - - `color?`: string (optional) - - `heightLevel?`: number (optional) + - `electricalProfile`: string + - `color?`: string (optional) + - `heightLevel?`: number (optional) - `SpeedLimitTagValues` - - `tag`: string - - `color`: string + - `tag`: string + - `color`: string - `ElectrificationValues` - - `type`: 'electrification' | 'neutral_section' - - `voltage?`: '1500V' | '25000V' (optional) - - `lowerPantograph?`: boolean (optional) + - `type`: 'electrification' | 'neutral_section' + - `voltage?`: '1500V' | '25000V' (optional) + - `lowerPantograph?`: boolean (optional) Make sure to replace yourData and yourTranslations with your actual data and translation objects. ## Adding Translations -The `SpeedSpaceChart` component supports internationalization by allowing you to provide custom translations for various UI elements. This feature enables you to tailor the chart to different languages and locales, enhancing the user experience for a global audience. +The `SpeedSpaceChart` component supports internationalization by allowing you to provide custom +translations for various UI elements. This feature enables you to tailor the chart to different +languages and locales, enhancing the user experience for a global audience. ### Define Your Translations - Create an object that contains key-value pairs for each text string you wish to override. The keys should match the expected identifiers used by the `SpeedSpaceChart` component, and the values should be the translated strings. +Create an object that contains key-value pairs for each text string you wish to override. The keys +should match the expected identifiers used by the `SpeedSpaceChart` component, and the values should +be the translated strings. Example translation object for French: @@ -114,12 +125,15 @@ const yourTranslations = { ## Visualization -The `ui-speedspacechart` component can be observed and manipulated on Storybook at this address: [storybook/speedspacechart](https://openrailassociation.github.io/osrd-ui/?path=/story/speedspacechart-rendering--speed-space-chart-default) +The `ui-speedspacechart` component can be observed and manipulated on Storybook at this address: +[storybook/speedspacechart](https://openrailassociation.github.io/osrd-ui/?path=/story/speedspacechart-rendering--speed-space-chart-default) ## Contributing -Contributions are welcome! Please refer to the repository's main README.md and CODE_OF_CONDUCT.md for more details on how to contribute. +Contributions are welcome! Please refer to the repository's main README.md and CODE_OF_CONDUCT.md +for more details on how to contribute. ## License -This project is licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 - see the LICENSE file for details. +This project is licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 - see +the LICENSE file for details. diff --git a/ui-speedspacechart/src/__tests__/utils.spec.ts b/ui-speedspacechart/src/__tests__/utils.spec.ts index cd822197..e54a79de 100644 --- a/ui-speedspacechart/src/__tests__/utils.spec.ts +++ b/ui-speedspacechart/src/__tests__/utils.spec.ts @@ -31,9 +31,11 @@ const store: Store = { stops: [], electrifications: [], slopes: [], + mrsp: undefined, powerRestrictions: [], electricalProfiles: [], speedLimitTags: [], + trainLength: 400, ratioX: 1, leftOffset: 0, cursor: { @@ -55,7 +57,6 @@ const store: Store = { declivities: false, speedLimitTags: false, steps: true, - temporarySpeedLimits: false, }, isSettingsPanelOpened: false, }; diff --git a/ui-speedspacechart/src/components/SpeedSpaceChart.tsx b/ui-speedspacechart/src/components/SpeedSpaceChart.tsx index 0304425b..3ef7d31f 100644 --- a/ui-speedspacechart/src/components/SpeedSpaceChart.tsx +++ b/ui-speedspacechart/src/components/SpeedSpaceChart.tsx @@ -5,7 +5,6 @@ import SettingsPanel from './common/SettingsPanel'; import { LINEAR_LAYERS_HEIGHTS, MARGINS } from './const'; import { resetZoom } from './helpers/layersManager'; import { - StepsLayer, AxisLayerY, CurveLayer, DeclivityLayer, @@ -13,7 +12,9 @@ import { FrontInteractivityLayer, PowerRestrictionsLayer, ReticleLayer, + SpeedLimitsLayer, SpeedLimitTagsLayer, + StepsLayer, TickLayerX, TickLayerYRight, } from './layers/index'; @@ -62,9 +63,11 @@ const SpeedSpaceChart = ({ stops: [], electrifications: [], slopes: [], + mrsp: undefined, powerRestrictions: undefined, electricalProfiles: undefined, speedLimitTags: undefined, + trainLength: 0, ratioX: 1, leftOffset: 0, cursor: { @@ -82,7 +85,6 @@ const SpeedSpaceChart = ({ steps: true, declivities: false, speedLimits: false, - temporarySpeedLimits: false, electricalProfiles: false, powerRestrictions: false, speedLimitTags: false, @@ -122,25 +124,10 @@ const SpeedSpaceChart = ({ }; useEffect(() => { - const storeData = { - speeds: data.speeds || [], - ecoSpeeds: data.ecoSpeeds || [], - stops: data.stops || [], - electrifications: data.electrifications || [], - slopes: data.slopes || [], - electricalProfiles: data.electricalProfiles, - powerRestrictions: data.powerRestrictions, - speedLimitTags: data.speedLimitTags, - }; - - const { speeds, ecoSpeeds, stops, electrifications, slopes } = storeData; - - if (speeds && ecoSpeeds && stops && electrifications && slopes) { - setStore((prev) => ({ - ...prev, - ...storeData, - })); - } + setStore((prev) => ({ + ...prev, + ...data, + })); }, [data]); useEffect(() => { @@ -181,6 +168,9 @@ const SpeedSpaceChart = ({ )} + {store.layersDisplay.speedLimits && ( + + )} {store.layersDisplay.steps && ( )} diff --git a/ui-speedspacechart/src/components/common/SettingsPanel.tsx b/ui-speedspacechart/src/components/common/SettingsPanel.tsx index a1d7f3c2..dbad6329 100644 --- a/ui-speedspacechart/src/components/common/SettingsPanel.tsx +++ b/ui-speedspacechart/src/components/common/SettingsPanel.tsx @@ -6,7 +6,7 @@ import { X } from '@osrd-project/ui-icons'; import type { Store } from '../../types/chartTypes'; import { DETAILS_BOX_SELECTION, LAYERS_SELECTION } from '../const'; import type { SpeedSpaceChartProps } from '../SpeedSpaceChart'; -import { checkLayerData, getAdaptiveHeight } from '../utils'; +import { isLayerActive, getAdaptiveHeight } from '../utils'; const SETTINGS_PANEL_BASE_HEIGHT = 442; const SPEEDSPACECHART_BASE_HEIGHT = 521.5; @@ -64,7 +64,7 @@ const SettingsPanel = ({ { setStore((prev) => ({ ...prev, diff --git a/ui-speedspacechart/src/components/const.ts b/ui-speedspacechart/src/components/const.ts index fe6431cd..e826a580 100644 --- a/ui-speedspacechart/src/components/const.ts +++ b/ui-speedspacechart/src/components/const.ts @@ -61,7 +61,6 @@ export const LAYERS_SELECTION: Array = [ 'steps', 'declivities', 'speedLimits', - 'temporarySpeedLimits', 'electricalProfiles', 'powerRestrictions', 'speedLimitTags', @@ -74,6 +73,7 @@ export const WHITE = chroma(255, 255, 255); export const GREY_50 = chroma(121, 118, 113); export const GREY_80 = chroma(49, 46, 43); export const LIGHT_BLUE = chroma(33, 112, 185); +export const ERROR_60 = chroma(217, 28, 28); /** * COLOR_DICTIONARY maps specific colors to their corresponding secondary colors used for speed limit tags. diff --git a/ui-speedspacechart/src/components/helpers/drawElements/speedLimits.ts b/ui-speedspacechart/src/components/helpers/drawElements/speedLimits.ts new file mode 100644 index 00000000..1c43ba1a --- /dev/null +++ b/ui-speedspacechart/src/components/helpers/drawElements/speedLimits.ts @@ -0,0 +1,33 @@ +import type { DrawFunctionParams } from '../../../types/chartTypes'; +import { ERROR_60, MARGINS } from '../../const'; +import { clearCanvas, positionToPosX, maxPositionValue, maxSpeedValue } from '../../utils'; + +const { CURVE_MARGIN_TOP } = MARGINS; + +export const drawSpeedLimits = ({ ctx, width, height, store }: DrawFunctionParams) => { + const { mrsp, trainLength, ratioX, leftOffset } = store; + const maxSpeed = maxSpeedValue(store); + + clearCanvas(ctx, width, height); + + ctx.save(); + ctx.translate(leftOffset, 0); + + const maxPosition = maxPositionValue(store); + + // TODO: draw speed limits + ctx.beginPath(); + ctx.lineWidth = 1; + ctx.lineCap = 'round'; + ctx.strokeStyle = ERROR_60.hex(); + + const adjustedHeight = height - CURVE_MARGIN_TOP; + const y = height - (200 / maxSpeed) * adjustedHeight; + const xStart = positionToPosX(0, maxPosition, width, ratioX); + ctx.lineTo(xStart, y); + const xEnd = positionToPosX(maxPosition, maxPosition, width, ratioX); + ctx.lineTo(xEnd, y); + ctx.stroke(); + + ctx.restore(); +}; diff --git a/ui-speedspacechart/src/components/layers/SpeedLimitsLayer.tsx b/ui-speedspacechart/src/components/layers/SpeedLimitsLayer.tsx new file mode 100644 index 00000000..85021ac0 --- /dev/null +++ b/ui-speedspacechart/src/components/layers/SpeedLimitsLayer.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import type { Store } from '../../types/chartTypes'; +import { drawSpeedLimits } from '../helpers/drawElements/speedLimits'; +import { useCanvas } from '../hooks'; + +type SpeedLimitsLayerProps = { + width: number; + height: number; + store: Store; +}; + +const SpeedLimitsLayer = ({ width, height, store }: SpeedLimitsLayerProps) => { + const canvas = useCanvas(drawSpeedLimits, { width, height, store }); + + return ( + + ); +}; + +export default SpeedLimitsLayer; diff --git a/ui-speedspacechart/src/components/layers/index.ts b/ui-speedspacechart/src/components/layers/index.ts index 8d3874f1..202e7421 100644 --- a/ui-speedspacechart/src/components/layers/index.ts +++ b/ui-speedspacechart/src/components/layers/index.ts @@ -1,4 +1,5 @@ export { default as StepsLayer } from './StepsLayer'; +export { default as SpeedLimitsLayer } from './SpeedLimitsLayer'; export { default as CurveLayer } from './CurveLayer'; export { default as DeclivityLayer } from './DeclivityLayer'; export { default as ElectricalProfileLayer } from './ElectricalProfileLayer'; diff --git a/ui-speedspacechart/src/components/utils.ts b/ui-speedspacechart/src/components/utils.ts index 465f78f9..fa02343b 100644 --- a/ui-speedspacechart/src/components/utils.ts +++ b/ui-speedspacechart/src/components/utils.ts @@ -169,16 +169,17 @@ export const drawLinearLayerBackground = ( }; /** - * Check if an optional layer data is missing in the store. - * Optional datas : electricalProfiles, powerRestrictions, speedLimitTags + * Return wether a layer should be active or not. + * Depending on the available data, some layers should be disabled. */ -export const checkLayerData = (store: Store, selection: (typeof LAYERS_SELECTION)[number]) => - (selection === 'speedLimits' || - selection === 'temporarySpeedLimits' || - selection === 'electricalProfiles' || - selection === 'powerRestrictions' || - selection === 'speedLimitTags') && - !store[selection]; +export const isLayerActive = (store: Store, selection: (typeof LAYERS_SELECTION)[number]) => { + if (selection === 'speedLimits') return store['mrsp']; + if (selection === 'electricalProfiles') return store['electricalProfiles']; + if (selection === 'powerRestrictions') return store['powerRestrictions']; + if (selection === 'speedLimitTags') return store['speedLimitTags']; + return true; +}; + /** * Given a store including a list of slopes, return the position and value of min and max slopes * @param store diff --git a/ui-speedspacechart/src/stories/assets/simulation_PMP_LM.ts b/ui-speedspacechart/src/stories/assets/simulation_PMP_LM.ts index 6f89c5bd..53301827 100644 --- a/ui-speedspacechart/src/stories/assets/simulation_PMP_LM.ts +++ b/ui-speedspacechart/src/stories/assets/simulation_PMP_LM.ts @@ -1,7 +1,10 @@ import type { Simulation } from '../../types/simulationTypes'; export const simulationPmpLm: Simulation = { - status: 'success', + mrsp: { + boundaries: [1000000, 1200000, 1800000, 3550000, 4500000], + values: [16.667, 27.778, 16.667, 27.778, 8.333, 83.333], + }, base: { positions: [ 0, 819, 3276, 7366, 29415, 52245, 98601, 159332, 207711, 357262, 1019000, 1142973, 1228000, diff --git a/ui-speedspacechart/src/stories/utils.ts b/ui-speedspacechart/src/stories/utils.ts index 237c0852..fd23ea14 100644 --- a/ui-speedspacechart/src/stories/utils.ts +++ b/ui-speedspacechart/src/stories/utils.ts @@ -28,6 +28,17 @@ const formatEcoSpeeds = (simulationFinalOutput: Simulation['final_output']) => { })); }; +const formatMrsp = (mrsp: Simulation['mrsp']) => { + const { boundaries, values } = mrsp; + return { + boundaries: boundaries.map(convertMmToKM), + values: values.map((speed) => ({ + speed: convertMsToKmh(speed), + isTemporary: false, + })), + }; +}; + const formatStops = (operationalPoints: PathProperties['operational_points']) => operationalPoints.map(({ position, extensions }) => ({ position: { @@ -137,21 +148,25 @@ export const formatData = ( ): Data => { const speeds = formatSpeed(simulation.base); const ecoSpeeds = formatEcoSpeeds(simulation.final_output); + const mrsp = formatMrsp(simulation.mrsp); const stops = formatStops(pathProperties.operational_points); const electrifications = formatElectrifications(pathProperties.electrifications); const slopes = formatSlopes(pathProperties.slopes); const powerRestrictions = formatPowerRestrictions(powerRestrictionsData); const electricalProfiles = formatElectricalProfiles(simulation, electrifications); const speedLimitTags = formatSpeedLimitTags(speedLimitTagsData); + const trainLength = 400; return { speeds, ecoSpeeds, + mrsp, stops, electrifications, slopes, electricalProfiles, powerRestrictions, speedLimitTags, + trainLength, }; }; diff --git a/ui-speedspacechart/src/styles/main.css b/ui-speedspacechart/src/styles/main.css index ca2854eb..9b0955b0 100644 --- a/ui-speedspacechart/src/styles/main.css +++ b/ui-speedspacechart/src/styles/main.css @@ -17,7 +17,6 @@ } #curve-layer, -#step-layer, #declivity-layer, #front-interactivity-layer { margin-left: 3rem; diff --git a/ui-speedspacechart/src/types/chartTypes.ts b/ui-speedspacechart/src/types/chartTypes.ts index 5e31fb5d..162a5666 100644 --- a/ui-speedspacechart/src/types/chartTypes.ts +++ b/ui-speedspacechart/src/types/chartTypes.ts @@ -21,6 +21,7 @@ export type ElectrificationValues = { lowerPantograph?: boolean; }; +// TODO: Stop using this type and use ValuesAlongPath instead. export type LayerData = { position: { start: number; @@ -29,17 +30,34 @@ export type LayerData = { value: T; }; +export type ValuesAlongPath = { + // The n boundaries of the values along the path. + // Ignore first and last values which are 0 and the total length of the path. + boundaries: number[]; + // The n+1 values along the path. Each value is associated with a range of the path. + // A value at index i is associated with the path between boundaries[i-1] bounradaires[i]. + values: T[]; +}; + +export type SpeedLimit = { + // The speed limit in km/h. + speed: number; + // Is the speed limit temporary or permanent. + isTemporary: boolean; +}; + export type Data = { speeds: LayerData[]; ecoSpeeds: LayerData[]; stops: LayerData[]; electrifications: LayerData[]; slopes: LayerData[]; + mrsp?: ValuesAlongPath; electricalProfiles?: LayerData[]; powerRestrictions?: LayerData[]; speedLimitTags?: LayerData[]; - speedLimits?: LayerData[]; - temporarySpeedLimits?: LayerData[]; + // The length of the train in meters. + trainLength: number; }; export type Store = Data & { @@ -60,7 +78,6 @@ export type Store = Data & { steps: boolean; declivities: boolean; speedLimits: boolean; - temporarySpeedLimits: boolean; electricalProfiles: boolean; powerRestrictions: boolean; speedLimitTags: boolean; diff --git a/ui-speedspacechart/src/types/simulationTypes.ts b/ui-speedspacechart/src/types/simulationTypes.ts index c34c3a3a..a7f4194a 100644 --- a/ui-speedspacechart/src/types/simulationTypes.ts +++ b/ui-speedspacechart/src/types/simulationTypes.ts @@ -1,7 +1,6 @@ import { type electricalProfilesDesignValues } from '../stories/assets/const'; export type Simulation = { - status: string; base: { positions: number[]; speeds: number[]; @@ -18,6 +17,10 @@ export type Simulation = { handled?: boolean; }[]; }; + mrsp: { + boundaries: number[]; + values: number[]; + }; }; export type PathProperties = {