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

fix lineString crash #2077

Merged
merged 10 commits into from
Oct 13, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -258,14 +258,14 @@ export const interpolateOnTime = (dataSimulation, keyValues, listValues, timePos
// If not array of array
if (listValue === 'speed' || listValue === 'speeds') {
if (
dataSimulation[listValue] &&
dataSimulation[listValue][0] &&
timePositionLocal >= dataSimulation[listValue][0][keyValues[0]]
dataSimulation?.[listValue] &&
dataSimulation?.[listValue][0] &&
timePositionLocal >= dataSimulation?.[listValue][0][keyValues[0]]
) {
const index = bisect(dataSimulation[listValue], timePositionLocal, 1);
bisection = [dataSimulation[listValue][index - 1], dataSimulation[listValue][index]];
}
} else if (dataSimulation[listValue]) {
} else if (dataSimulation?.[listValue]) {
// Array of array
dataSimulation[listValue].forEach((section) => {
const index = bisect(section, timePositionLocal, 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ export default function RenderItinerary() {
</Source>
);
}
return '';
return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,5 @@ export default function RenderItineraryMarkers() {
});
}

return markers.map((marker) => marker);
return <>{markers.map((marker) => marker)}</>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,15 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { Layer, Source, Marker } from 'react-map-gl';
import lineSliceAlong from '@turf/line-slice-along';
import { Point, Feature, LineString } from 'geojson';
import length from '@turf/length';
import { Feature, LineString } from 'geojson';
import cx from 'classnames';

import { RootState } from 'reducers';
import { datetime2time } from 'utils/timeManipulation';

interface TrainPosition {
id: string;
headPosition: Feature<Point>;
tailPosition: Feature<Point>;
headDistanceAlong: number;
tailDistanceAlong: number;
speedTime: {
speed: number;
time: number;
};
trainLength: number;
}
import { boundedValue } from 'utils/numbers';
import { Viewport } from 'reducers/map';
import { TrainPosition } from './types';

function getFill(isSelectedTrain: boolean, ecoBlocks) {
if (isSelectedTrain) {
Expand Down Expand Up @@ -54,27 +45,23 @@ function getSpeedAndTimeLabel(isSelectedTrain, ecoBlocks, point: TrainPosition)
}

// When the train is backward, lineSliceAlong will crash. we need to have head and tail in the right order
function makeDisplayedHeadAndTail(point: TrainPosition) {
export function makeDisplayedHeadAndTail(point: TrainPosition, geojsonPath: Feature<LineString>) {
const pathLength = length(geojsonPath);
const trueHead = Math.max(point.tailDistanceAlong, point.headDistanceAlong);
const trueTail = Math.max(trueHead - point.trainLength, 0);
const head = Math.max(trueHead, trueTail);
const tail = Math.min(trueHead, trueTail);
return { tail, head };
const trueTail = Math.min(point.tailDistanceAlong, point.headDistanceAlong);
return {
head: boundedValue(trueHead, 0, pathLength),
tail: boundedValue(trueTail, 0, pathLength),
};
}

function getLengthFactorToKeepLabelPlacedCorrectlyWhenZooming(
viewport: {
zoom: number;
transformRequest: (url: string, resourceType: string, urlmap: string) => any;
},
threshold = 12
) {
function getLengthFactorToKeepLabelPlacedCorrectlyWhenZooming(viewport: Viewport, threshold = 12) {
return 2 ** (threshold - viewport?.zoom);
}

interface TrainHoverPositionProps {
point: TrainPosition;
isSelectedTrain: boolean;
isSelectedTrain?: boolean;
geojsonPath: Feature<LineString>;
}

Expand All @@ -91,12 +78,12 @@ function TrainHoverPosition(props: TrainHoverPositionProps) {
const simulation = useSelector((state: RootState) => state.osrdsimulation.simulation.present);
const trainID = simulation.trains[selectedTrain].id;
const { ecoBlocks } = allowancesSettings[trainID];
const fill = getFill(isSelectedTrain, ecoBlocks);
const fill = getFill(isSelectedTrain as boolean, ecoBlocks);
const label = getSpeedAndTimeLabel(isSelectedTrain, ecoBlocks, point);

if (geojsonPath && point.headDistanceAlong && point.tailDistanceAlong) {
const zoomLengthFactor = getLengthFactorToKeepLabelPlacedCorrectlyWhenZooming(viewport);
const { tail, head } = makeDisplayedHeadAndTail(point);
const { tail, head } = makeDisplayedHeadAndTail(point, geojsonPath);
const trainGeoJsonPath = lineSliceAlong(geojsonPath, tail, head);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { lineString, point, lengthToDegrees } from '@turf/helpers';

import { makeDisplayedHeadAndTail, TrainPosition } from '../TrainHoverPosition';

// test examples are given on a grid with kilometer unit
function convertKmCoordsToDegree(coords) {
return coords.map((v) => lengthToDegrees(v));
}

// test examples are given on a grid with kilometer unit
function pointFromKmCoords(coords) {
return point(convertKmCoordsToDegree(coords));
}

const unitKmSquare = [
convertKmCoordsToDegree([0, 0]),
convertKmCoordsToDegree([1, 0]),
convertKmCoordsToDegree([1, 1]),
convertKmCoordsToDegree([0, 1]),
];

const unitKmLine = [convertKmCoordsToDegree([0, 0]), convertKmCoordsToDegree([1, 0])];

describe('makeDisplayedHeadAndTail', () => {
describe('normal train', () => {
it('should return train head and tail to display', () => {
const trainPosition: TrainPosition = {
id: 'train',
headPosition: pointFromKmCoords([1, 0]),
tailPosition: pointFromKmCoords([0, 0]),
headDistanceAlong: 1,
tailDistanceAlong: 0,
speedTime: { speed: 0, time: 0 },
trainLength: 1,
};
const pathPoints = unitKmSquare;
const pathLineString = lineString(pathPoints);
const { head, tail } = makeDisplayedHeadAndTail(trainPosition, pathLineString);
expect(head).toEqual(1);
expect(tail).toEqual(0);
});
});
describe('backward train', () => {
it('should return train head and tail', () => {
const trainPosition: TrainPosition = {
id: 'train',
headPosition: pointFromKmCoords([0, 0]),
tailPosition: pointFromKmCoords([1, 0]),
headDistanceAlong: 1,
tailDistanceAlong: 0,
speedTime: { speed: 0, time: 0 },
trainLength: 1,
};
const pathPoints = unitKmSquare;
const pathLineString = lineString(pathPoints);
const { head, tail } = makeDisplayedHeadAndTail(trainPosition, pathLineString);
expect(head).toEqual(1);
expect(tail).toEqual(0);
});
});
describe('train outside of the path', () => {
it('should return train head and tail bounded in path', () => {
const trainPosition: TrainPosition = {
id: 'train',
headPosition: pointFromKmCoords([1, 0]),
tailPosition: pointFromKmCoords([2, 0]),
headDistanceAlong: 2,
tailDistanceAlong: 1,
speedTime: { speed: 0, time: 0 },
trainLength: 1,
};
const pathPoints = unitKmLine;
const pathLineString = lineString(pathPoints);
const { head, tail } = makeDisplayedHeadAndTail(trainPosition, pathLineString);
expect(head).toBeCloseTo(1, 6);
expect(tail).toBeCloseTo(1, 6);
});
});
});
14 changes: 14 additions & 0 deletions front/src/applications/osrd/components/SimulationMap/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Point, Feature } from 'geojson';

export interface TrainPosition {
id: string;
headPosition: Feature<Point>;
tailPosition: Feature<Point>;
headDistanceAlong: number;
tailDistanceAlong: number;
speedTime: {
speed: number;
time: number;
};
trainLength: number;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import 'common/Map/Map.scss';

import React, { useCallback, useEffect, useRef, useState } from 'react';
import ReactMapGL, { AttributionControl, FlyToInterpolator, ScaleControl } from 'react-map-gl';

import ReactMapGL, {
AttributionControl,
FlyToInterpolator,
ScaleControl,
MapRef,
MapEvent,
} from 'react-map-gl';
import { lineString as turfLineString, point as turfPoint } from '@turf/helpers';
import { useDispatch, useSelector } from 'react-redux';
import turfNearestPointOnLine, { NearestPointOnLine } from '@turf/nearest-point-on-line';
import { Feature, Position, LineString } from 'geojson';
import { useParams } from 'react-router-dom';

import { RootState } from 'reducers';
import { updateFeatureInfoClickOSRD } from 'reducers/osrdconf';
import { updateViewport } from 'reducers/map';

/* Main data & layers */
import Background from 'common/Map/Layers/Background';
Expand Down Expand Up @@ -34,26 +46,24 @@ import TracksGeographic from 'common/Map/Layers/TracksGeographic';
import TracksSchematic from 'common/Map/Layers/TracksSchematic';
import colors from 'common/Map/Consts/colors';
import osmBlankStyle from 'common/Map/Layers/osmBlankStyle';
import turfNearestPointOnLine from '@turf/nearest-point-on-line';
import { updateFeatureInfoClickOSRD } from 'reducers/osrdconf';
import { updateViewport } from 'reducers/map';
import { useParams } from 'react-router-dom';

import 'common/Map/Map.scss';

function Map() {
const { viewport, mapSearchMarker, mapStyle, mapTrackSources, showOSM, layersSettings } =
useSelector((state) => state.map);
const [idHover, setIdHover] = useState(undefined);
const [trackSectionHover, setTrackSectionHover] = useState(undefined);
const [lngLatHover, setLngLatHover] = useState(undefined);
const [trackSectionGeoJSON, setTrackSectionGeoJSON] = useState(undefined);
const [snappedPoint, setSnappedPoint] = useState(undefined);
const { urlLat, urlLon, urlZoom, urlBearing, urlPitch } = useParams();
useSelector((state: RootState) => state.map);
const [idHover, setIdHover] = useState();
const [trackSectionHover, setTrackSectionHover] = useState<Feature<any>>();
const [lngLatHover, setLngLatHover] = useState<Position>();
const [trackSectionGeoJSON, setTrackSectionGeoJSON] = useState<LineString>();
const [snappedPoint, setSnappedPoint] = useState<NearestPointOnLine>();
const { urlLat = '', urlLon = '', urlZoom = '', urlBearing = '', urlPitch = '' } = useParams();
const dispatch = useDispatch();
const updateViewportChange = useCallback(
(value) => dispatch(updateViewport(value, undefined)),
[dispatch]
);
const mapRef = useRef(null);
const mapRef = useRef<MapRef>(null);

const scaleControlStyle = {
left: 20,
Expand All @@ -63,8 +73,8 @@ function Map() {
const resetPitchBearing = () => {
updateViewportChange({
...viewport,
bearing: parseFloat(0),
pitch: parseFloat(0),
bearing: 0,
pitch: 0,
transitionDuration: 1000,
transitionInterpolator: new FlyToInterpolator(),
});
Expand Down Expand Up @@ -94,47 +104,49 @@ function Map() {
}
};

const getGeoJSONFeature = (e) => {
const getGeoJSONFeature = (e: MapEvent) => {
if (
trackSectionHover === undefined ||
e.features[0].properties.id !== trackSectionHover.properties.id
e?.features?.[0].properties.id !== trackSectionHover?.properties?.id
) {
setTrackSectionHover(e.features[0]);
setTrackSectionHover(e?.features?.[0]);
}

// Get GEOJSON of features hovered for snapping
const width = 5;
const height = 5;
const features = mapRef.current.queryRenderedFeatures(
[
[e.point[0] - width / 2, e.point[1] - height / 2],
[e.point[0] + width / 2, e.point[1] + height / 2],
],
{
layers:
mapTrackSources === 'geographic'
? ['chartis/tracks-geo/main']
: ['chartis/tracks-sch/main'],
if (mapRef.current) {
const features = mapRef.current.queryRenderedFeatures(
[
[e.point[0] - width / 2, e.point[1] - height / 2],
[e.point[0] + width / 2, e.point[1] + height / 2],
],
{
layers:
mapTrackSources === 'geographic'
? ['chartis/tracks-geo/main']
: ['chartis/tracks-sch/main'],
}
);
if (features[0] !== undefined) {
setTrackSectionGeoJSON(features[0].geometry);
}
);
if (features[0] !== undefined) {
setTrackSectionGeoJSON(features[0].geometry);
}
};

const onFeatureHover = (e) => {
if (e.features !== null && e.features[0] !== undefined) {
const onFeatureHover = (e: MapEvent) => {
if (e.features !== null && e?.features?.[0] !== undefined) {
getGeoJSONFeature(e);
setIdHover(e.features[0].properties.id);
setLngLatHover(e.lngLat);
setLngLatHover(e?.lngLat);
} else {
setIdHover(undefined);
setSnappedPoint(undefined);
}
};

const defineInteractiveLayers = () => {
const interactiveLayersLocal = [];
const interactiveLayersLocal: Array<string> = [];
if (mapTrackSources === 'geographic') {
interactiveLayersLocal.push('chartis/tracks-geo/main');
if (layersSettings.operationalpoints) {
Expand Down Expand Up @@ -215,7 +227,7 @@ function Map() {
{/* Have to duplicate objects with sourceLayer to avoid cache problems in mapbox */}
{mapTrackSources === 'geographic' ? (
<>
<TVDs geomType="geo" colors={colors[mapStyle]} idHover={idHover} />
<TVDs geomType="geo" idHover={idHover} />
<Catenaries geomType="geo" colors={colors[mapStyle]} />
<Platform colors={colors[mapStyle]} />
<TracksGeographic colors={colors[mapStyle]} />
Expand Down Expand Up @@ -247,7 +259,7 @@ function Map() {
<RenderItinerary />
<RenderItineraryMarkers />
{mapSearchMarker !== undefined ? (
<SearchMarker data={mapSearchMarker} colors={colors[mapStyle]} />
<SearchMarker data={mapSearchMarker as object} colors={colors[mapStyle]} />
) : null}
{snappedPoint !== undefined ? <SnappedMarker geojson={snappedPoint} /> : null}
</ReactMapGL>
Expand Down
Loading