-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
414 additions
and
130 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
front/src/common/Map/components/TrainOnMap/TrainOnMap.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import React, { useMemo } from 'react'; | ||
|
||
import type { Position } from '@turf/helpers'; | ||
import cx from 'classnames'; | ||
import type { Feature, LineString } from 'geojson'; | ||
import { Source, Marker } from 'react-map-gl/maplibre'; | ||
|
||
import type { SimulationResponseSuccess } from 'applications/operationalStudies/types'; | ||
import OrderedLayer from 'common/Map/Layers/OrderedLayer'; | ||
import { LAYERS, LAYER_GROUPS_ORDER } from 'config/layerOrder'; | ||
import type { Viewport } from 'reducers/map'; | ||
import { datetime2time } from 'utils/timeManipulation'; | ||
|
||
import { getTrainPieces } from './getTrainBody'; | ||
|
||
/** Information of the train at a precise moment */ | ||
export type TrainCurrentInfo = { | ||
trainId: number; | ||
headPositionCoord: Position; | ||
headDistanceAlong: number; // in km | ||
tailDistanceAlong: number; // in km | ||
speed: number; | ||
time: Date; | ||
}; | ||
|
||
const LABEL_SHIFT_FACTORS = { | ||
LONG: 0.005, | ||
LAT: 0.0011, | ||
}; | ||
|
||
const TrainLabel = ({ | ||
isEcoTrain, | ||
trainInfo, | ||
}: { | ||
isEcoTrain: boolean; | ||
trainInfo: TrainCurrentInfo; | ||
}) => ( | ||
<> | ||
<span | ||
className={cx( | ||
'small', | ||
'train-speed-label', | ||
'font-weight-bold', | ||
isEcoTrain ? 'text-secondary' : 'text-primary' | ||
)} | ||
> | ||
{Math.round(trainInfo.speed)} | ||
km/h | ||
</span> | ||
<span className="ml-2 small train-speed-label">{`${datetime2time(trainInfo.time)}`}</span> | ||
</> | ||
); | ||
|
||
const getZoomPowerOf2LengthFactor = (viewport: Viewport, threshold = 12) => | ||
2 ** (threshold - viewport.zoom); | ||
|
||
type TrainOnMapProps = { | ||
trainInfo: TrainCurrentInfo; | ||
trainSimulation: SimulationResponseSuccess; | ||
geojsonPath: Feature<LineString>; | ||
viewport: Viewport; | ||
}; | ||
|
||
// TO DO DROP V1: remove this comment | ||
// TrainOnMap corresponds to TrainHoverPositionV2 | ||
const TrainOnMap = ({ trainInfo, geojsonPath, viewport, trainSimulation }: TrainOnMapProps) => { | ||
const zoomLengthFactor = getZoomPowerOf2LengthFactor(viewport); | ||
|
||
const { trainBody, trainExtremities } = getTrainPieces(trainInfo, geojsonPath, zoomLengthFactor); | ||
|
||
const coordinates = useMemo( | ||
() => ({ | ||
lat: trainInfo.headPositionCoord[1] + zoomLengthFactor * LABEL_SHIFT_FACTORS.LAT, | ||
long: trainInfo.headPositionCoord[0] + zoomLengthFactor * LABEL_SHIFT_FACTORS.LONG, | ||
}), | ||
[trainInfo] | ||
); | ||
|
||
const isEcoTrain = useMemo( | ||
() => trainSimulation.base.energy_consumption < trainSimulation.final_output.energy_consumption, | ||
[trainSimulation] | ||
); | ||
|
||
return ( | ||
<> | ||
<Marker longitude={coordinates.long} latitude={coordinates.lat}> | ||
<TrainLabel isEcoTrain={isEcoTrain} trainInfo={trainInfo} /> | ||
</Marker> | ||
{trainExtremities.map((trainExtremity) => ( | ||
<Source | ||
type="geojson" | ||
data={trainExtremity.data} | ||
key={`${trainInfo.trainId}-${trainExtremity.name}`} | ||
> | ||
<OrderedLayer | ||
id={`${trainInfo.trainId}-${trainExtremity.name}`} | ||
type="fill" | ||
paint={{ | ||
'fill-color': '#303383', | ||
}} | ||
layerOrder={LAYER_GROUPS_ORDER[LAYERS.TRAIN.GROUP]} | ||
/> | ||
</Source> | ||
))} | ||
<Source type="geojson" data={trainBody.data}> | ||
<OrderedLayer | ||
id={`${trainInfo.trainId}-path`} | ||
type="line" | ||
paint={{ | ||
'line-width': 16, | ||
'line-color': '#303383', | ||
}} | ||
layerOrder={LAYER_GROUPS_ORDER[LAYERS.TRAIN.GROUP]} | ||
/> | ||
</Source> | ||
</> | ||
); | ||
}; | ||
|
||
export default TrainOnMap; |
134 changes: 134 additions & 0 deletions
134
front/src/common/Map/components/TrainOnMap/getTrainBody.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import along from '@turf/along'; | ||
import bezierSpline from '@turf/bezier-spline'; | ||
import { type Point, polygon, lineString } from '@turf/helpers'; | ||
import length from '@turf/length'; | ||
import lineSliceAlong from '@turf/line-slice-along'; | ||
import transformTranslate from '@turf/transform-translate'; | ||
import type { Feature, LineString } from 'geojson'; | ||
import { mapValues } from 'lodash'; | ||
|
||
import { getCurrentBearing } from 'utils/geometry'; | ||
import { clamp } from 'utils/numbers'; | ||
|
||
import type { TrainCurrentInfo } from './TrainOnMap'; | ||
|
||
type TriangleSideDimensions = { | ||
left: number; | ||
right: number; | ||
up: number; | ||
upWidth: number; | ||
down: number; | ||
}; | ||
|
||
// When the train is backward, lineSliceAlong will crash. we need to have head and tail in the right order | ||
export const computeExtremityPositionPoints = ( | ||
{ headDistanceAlong, tailDistanceAlong }: TrainCurrentInfo, | ||
geojsonPath: Feature<LineString>, | ||
sideDimensions: { | ||
head: TriangleSideDimensions; | ||
tail: TriangleSideDimensions; | ||
} | ||
) => { | ||
const headMinusTriangle = headDistanceAlong - sideDimensions.head.up; | ||
const tailPlusTriangle = Math.min( | ||
tailDistanceAlong + sideDimensions.tail.down, | ||
headMinusTriangle | ||
); | ||
|
||
const pathLength = length(geojsonPath); | ||
const headDistance = clamp(headMinusTriangle, [0, pathLength]); | ||
const tailDistance = clamp(tailPlusTriangle, [0, pathLength]); | ||
|
||
const headPositionPoint = along(geojsonPath, headDistance); | ||
const tailPositionPoint = along(geojsonPath, tailDistance); | ||
|
||
return { | ||
headDistance, | ||
tailDistance, | ||
headPositionPoint, | ||
tailPositionPoint, | ||
}; | ||
}; | ||
|
||
const getTriangleSideDimensions = (zoomLengthFactor: number, size = 2) => { | ||
const scaleNumber = (x: number) => x * zoomLengthFactor * size; | ||
const head = { | ||
left: 0.05, | ||
right: 0.05, | ||
up: 0.1, | ||
upWidth: 0.019, | ||
down: 0.02, | ||
}; | ||
const tail = { | ||
...head, | ||
up: 0.05, | ||
}; | ||
return { | ||
head: mapValues(head, scaleNumber), | ||
tail: mapValues(tail, scaleNumber), | ||
}; | ||
}; | ||
|
||
const getTriangle = ( | ||
trainGeoJsonPath: Feature<LineString>, | ||
position: Feature<Point>, | ||
sideDimensions: Record<string, number> | ||
) => { | ||
const bearing = getCurrentBearing(trainGeoJsonPath); | ||
const left = transformTranslate(position, sideDimensions.left, bearing - 90); | ||
const right = transformTranslate(position, sideDimensions.right, bearing + 90); | ||
const up = transformTranslate(position, sideDimensions.up, bearing); | ||
const down = transformTranslate(position, sideDimensions.down, bearing + 180); | ||
const upLeft = transformTranslate(up, sideDimensions.upWidth, bearing - 90); | ||
const upRight = transformTranslate(up, sideDimensions.upWidth, bearing + 90); | ||
const coordinates = [ | ||
down.geometry.coordinates, | ||
left.geometry.coordinates, | ||
upLeft.geometry.coordinates, | ||
upRight.geometry.coordinates, | ||
right.geometry.coordinates, | ||
down.geometry.coordinates, | ||
]; | ||
const contour = lineString(coordinates); | ||
const bezier = bezierSpline(contour); | ||
const triangle = polygon([bezier.geometry.coordinates]); | ||
return triangle; | ||
}; | ||
|
||
const getTrainGeoJsonPath = ( | ||
geojsonPath: Feature<LineString>, | ||
tailDistance: number, | ||
headDistance: number | ||
) => { | ||
const threshold = 0.0005; | ||
if (headDistance - tailDistance > threshold) { | ||
return lineSliceAlong(geojsonPath, tailDistance, headDistance); | ||
} | ||
if (headDistance > threshold) { | ||
return lineSliceAlong(geojsonPath, headDistance - threshold, headDistance); | ||
} | ||
return lineSliceAlong(geojsonPath, 0, threshold); | ||
}; | ||
|
||
export const getTrainPieces = ( | ||
trainInfo: TrainCurrentInfo, | ||
geojsonPath: Feature<LineString>, | ||
zoomLengthFactor: number | ||
) => { | ||
const sideDimensions = getTriangleSideDimensions(zoomLengthFactor); | ||
|
||
const { tailDistance, headDistance, headPositionPoint, tailPositionPoint } = | ||
computeExtremityPositionPoints(trainInfo, geojsonPath, sideDimensions); | ||
|
||
const trainGeoJsonPath = getTrainGeoJsonPath(geojsonPath, tailDistance, headDistance); | ||
const headTriangle = getTriangle(trainGeoJsonPath, headPositionPoint, sideDimensions.head); | ||
const rearTriangle = getTriangle(trainGeoJsonPath, tailPositionPoint, sideDimensions.tail); | ||
|
||
return { | ||
trainBody: { name: 'path', data: trainGeoJsonPath }, | ||
trainExtremities: [ | ||
{ name: 'head', data: headTriangle }, | ||
{ name: 'tail', data: rearTriangle }, | ||
], | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
...odules/simulationResult/components/SimulationResultsMap/getSelectedTrainHoverPositions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import along from '@turf/along'; | ||
import { lineString } from '@turf/helpers'; | ||
import type { Feature, LineString } from 'geojson'; | ||
import { max, min } from 'lodash'; | ||
|
||
import type { TrainCurrentInfo } from 'common/Map/components/TrainOnMap/TrainOnMap'; | ||
import type { PositionsSpeedTimes } from 'reducers/osrdsimulation/types'; | ||
import { mToKm } from 'utils/physics'; | ||
|
||
const getSelectedTrainHoverPositions = ( | ||
geojsonPath: Feature<LineString>, | ||
positionValues: PositionsSpeedTimes<Date>, | ||
trainId: number | ||
): TrainCurrentInfo | undefined => { | ||
const { headPosition, tailPosition } = positionValues; | ||
|
||
if (headPosition === undefined || tailPosition === undefined) { | ||
return undefined; | ||
} | ||
|
||
const headDistanceAlong = mToKm(headPosition.position); | ||
const tailDistanceAlong = mToKm(tailPosition.position); | ||
|
||
const line = lineString(geojsonPath.geometry.coordinates); | ||
const headPositionPoint = along(line, headDistanceAlong, { | ||
units: 'kilometers', | ||
}); | ||
|
||
return { | ||
trainId, | ||
headPositionCoord: headPositionPoint.geometry.coordinates, | ||
headDistanceAlong: max([headDistanceAlong, tailDistanceAlong])!, | ||
tailDistanceAlong: min([headDistanceAlong, tailDistanceAlong])!, | ||
speed: positionValues.speed.speed, | ||
time: positionValues.speed.time, | ||
}; | ||
}; | ||
|
||
export default getSelectedTrainHoverPositions; |
Oops, something went wrong.