-
Notifications
You must be signed in to change notification settings - Fork 74
Expand file tree
/
Copy pathgeometry.ts
More file actions
170 lines (155 loc) · 6 KB
/
geometry.ts
File metadata and controls
170 lines (155 loc) · 6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
import along from '@turf/along';
import turfDistance from '@turf/distance';
import { point, featureCollection, lineString, type Units } from '@turf/helpers';
import { getCoord } from '@turf/invariant';
import length from '@turf/length';
import type {
Feature,
Point,
FeatureCollection,
LineString,
MultiLineString,
Position,
} from 'geojson';
import { minBy } from 'lodash';
import type { GeoJsonLineString } from 'common/api/osrdEditoastApi';
export function getTangent(
tangentPoint: Position,
line: Feature<LineString>
): FeatureCollection<Point> {
const index = line.geometry.coordinates.findIndex(
(pos) => pos[0] === tangentPoint[0] && pos[1] === tangentPoint[1]
);
let index1 = index - 1;
let index2 = index + 1;
if (index === 0) {
index1 = 0;
index2 = 1;
}
if (index === line.geometry.coordinates.length - 1) {
index1 = index - 1;
index2 = index;
}
const point1 = point(line.geometry.coordinates[index1]);
const point2 = point(line.geometry.coordinates[index2]);
return featureCollection([point1, point2]);
}
export function lengthFromLineCoordinates(coordinates?: Position[]) {
if (coordinates) {
return length(lineString(coordinates));
}
return NaN;
}
export type NearestPointOnLine = Feature<Point> & {
properties: {
index: number;
dist: number;
location: number;
};
};
/**
* This function has the same signature than the one of turf, but it is more precise.
*
* You can check those links for references :
* - https://github.com/Turfjs/turf/issues/1440
* - https://github.com/Turfjs/turf/issues/2023
*
* For each segment, we compute the height from the point of the triangle
* composed of the segment (BC) and the point (A),
* and we check if A' (projection of A on BC) is in the segment (BC) by computing the angle for B & C
*
* Math reminder on triangle A,B & C where AB is the lenght of the segment:
* - The height from the vextex A is `Ha = AB * SIN(AB,BC)`
* Angle of vertex B (cosines law) is `AngleB = arccos((AB² + BC² - AC²) / (2 * AB * BC))`
*/
export function nearestPointOnLine(
line: Feature<LineString> | LineString | Feature<MultiLineString> | MultiLineString,
inPoint: Feature<Point> | Point | Position,
options?: { units?: Units }
): NearestPointOnLine {
// Handle multilines
if (
line.type === 'MultiLineString' ||
(line.type === 'Feature' && line.geometry.type === 'MultiLineString')
) {
const linesCoords =
line.type === 'MultiLineString'
? line.coordinates
: (line as Feature<MultiLineString>).geometry.coordinates;
const nearestPoints = linesCoords.map((lineCoords) =>
nearestPointOnLine({ type: 'LineString', coordinates: lineCoords }, inPoint, options)
);
return minBy(nearestPoints, (i) => i.properties.dist) || nearestPoints[0];
}
const pointA = getCoord(inPoint);
const lineCoords: Position[] =
line.type === 'Feature' ? (line as Feature<LineString>).geometry.coordinates : line.coordinates;
const closestPointPerSegment: Array<{ point: Position; dist: number; index: number }> = [];
for (let n = 1; n < lineCoords.length; n += 1) {
const pointB = lineCoords[n - 1];
const pointC = lineCoords[n];
const lengthAB = turfDistance(pointA, pointB);
if (lengthAB === 0) {
closestPointPerSegment.push({ point: pointB, index: n - 1, dist: lengthAB });
} else {
const lengthAC = turfDistance(pointA, pointC);
const lengthBC = turfDistance(pointB, pointC);
const angleB = Math.acos(
(lengthAB ** 2 + lengthBC ** 2 - lengthAC ** 2) / (2 * lengthAB * lengthBC)
);
const heightA = lengthAB * Math.sin(angleB);
// we compute the BA'
const lengthBAPrime = lengthAB * Math.cos(angleB);
if (angleB >= 90 * (Math.PI / 180)) {
// Closest is B : projection A' is outside BC, and before B
closestPointPerSegment.push({ point: pointB, index: n - 1, dist: lengthAB });
} else if (lengthBAPrime > lengthBC) {
// Closest is C : projection A' is outside BC, and after C
closestPointPerSegment.push({ point: pointC, index: n - 1, dist: lengthAC });
} else {
// Closest is in BC
// we do the ratio BA' / BC
const ratio = lengthBAPrime / lengthBC;
// We reduce the vector AB by the ratio
const vectorBAPrime = [(pointC[0] - pointB[0]) * ratio, (pointC[1] - pointB[1]) * ratio];
// The new result is point B on which we apply the vector
const pointOnSegmentLine = [pointB[0] + vectorBAPrime[0], pointB[1] + vectorBAPrime[1]];
closestPointPerSegment.push({ point: pointOnSegmentLine, index: n - 1, dist: heightA });
}
}
}
const result = minBy(closestPointPerSegment, 'dist') || {
point: lineCoords[0],
index: 0,
dist: turfDistance(pointA, lineCoords[0]),
};
return point(result.point, {
dist: result.dist,
index: result.index,
location: length(lineString([...lineCoords.slice(0, result.index + 1), result.point]), options),
});
}
/**
* Given a track's geometry and length and an offset from the start of the track (in mm),
* returns the coordinates of the point for the given offset.
*/
export function getPointOnTrackCoordinates(
geometry: GeoJsonLineString,
trackLength: number, // in mm
infraPositionOnTrack: number // in mm
): Position {
const pathLineString = lineString(geometry.coordinates);
const geometryTrackLength = length(pathLineString, { units: 'millimeters' });
const infraTrackLength = trackLength;
// TODO TS2 : when adapting train update check that this computation works properly
const geometryDistanceAlongTrack =
infraPositionOnTrack * (geometryTrackLength / infraTrackLength);
return along(pathLineString, geometryDistanceAlongTrack, { units: 'millimeters' }).geometry
.coordinates;
}
export function getBarycenter(coords: Position[]): Position {
const n = coords.length;
if (n === 0) throw new Error('No coordinates provided');
const sum = coords.reduce((acc, [lon, lat]) => [acc[0] + lon, acc[1] + lat], [0, 0]);
return [sum[0] / n, sum[1] / n];
}