Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ data class TrainPathImpl(
return distanceRangeMapOf(entries)
}

private fun getTrackRanges(): List<DirTrackRange> {
override fun getTrackRanges(): List<DirTrackRange> {
val partialTrackRanges = mutableListOf<PartialDirTrackRange>()
for (chunkRange in chunks) {
val dirChunkId = chunkRange.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ interface TrainPath : PhysicsPath, PathProperties {
fun getZonePaths(): List<ZonePathRange>

fun getZoneRanges(): List<ZoneRange>

fun getTrackRanges(): List<DirTrackRange>
// To be expanded as needed with other linear objects
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ data class LineString(
for (i in 0..<bufferLon.size - 1) {
cumulativeLength +=
Point(bufferLat[i], bufferLon[i])
.distanceAsMeters(Point(bufferLat[i + 1], bufferLon[i + 1]))
.haversineDistanceAsMeters(Point(bufferLat[i + 1], bufferLon[i + 1]))
cumulativeLengths[i] = cumulativeLength
}
return LineString(bufferLat, bufferLon, cumulativeLengths)
Expand Down Expand Up @@ -236,7 +236,7 @@ data class LineString(
newBufferLat.get(newBufferLat.size() - 1),
newBufferLon.get(newBufferLon.size() - 1),
)
.distanceAsMeters(
.haversineDistanceAsMeters(
Point(lineString.bufferLat[0], lineString.bufferLon[0])
)

Expand Down
21 changes: 21 additions & 0 deletions core/osrd-geom/src/main/kotlin/fr/sncf/osrd/geom/Point.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package fr.sncf.osrd.geom

import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt

const val AVERAGE_EARTH_RADIUS = 6371008.8

@JvmRecord
data class Point(@JvmField val lat: Double, @JvmField val lon: Double) {
/**
Expand All @@ -19,6 +24,22 @@ data class Point(@JvmField val lat: Double, @JvmField val lon: Double) {
return WGS84Interpolator.EARTH_RADIUS * sqrt(xDiff * xDiff + yDiff * yDiff)
}

/**
* Returns the distance between this point and another point in meters, using the
* [haversine formula](https://en.wikipedia.org/wiki/Haversine_formula), to match the distances
* used in the frontend.
*/
fun haversineDistanceAsMeters(other: Point): Double {
val dLon = Math.toRadians(other.lon - lon)
val dLat = Math.toRadians(other.lat - lat)
val lat1 = Math.toRadians(lat)
val lat2 = Math.toRadians(other.lat)

val a = sin(dLat / 2).pow(2) + sin(dLon / 2).pow(2) * cos(lat1) * cos(lat2)
val theta = 2 * atan2(sqrt(a), sqrt(1 - a))
return theta * AVERAGE_EARTH_RADIUS
}

override fun toString(): String {
return String.format("{lat=%f, lon=%f}", lat, lon)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import fr.sncf.osrd.api.RangeValues
import fr.sncf.osrd.path.interfaces.PhysicsPath
import fr.sncf.osrd.railjson.schema.geom.RJSLineString
import fr.sncf.osrd.utils.json.UnitAdapterFactory
import fr.sncf.osrd.utils.units.Distance
import fr.sncf.osrd.utils.units.Offset

class PathPropResponse(
Expand All @@ -18,6 +19,7 @@ class PathPropResponse(
val geometry: RJSLineString,
@Json(name = "operational_points") val operationalPoints: List<OperationalPointResponse>,
val zones: RangeValues<String>,
@Json(name = "geom_projection") val geomProjection: GeometricProjection,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please comment here that Haversine formula is used.

)

interface Electrification
Expand Down Expand Up @@ -54,6 +56,22 @@ data class OperationalPointPartExtension(val sncf: OperationalPointPartSncfExten

data class OperationalPointPartSncfExtension(val kp: String)

data class GeometricProjection(
@Json(name = "topo_offsets") val topoOffsets: List<Offset<PhysicsPath>>,
@Json(name = "geom_offsets") val geomOffsets: List<Offset<RJSLineString>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's about to become RJSMutliLineString 😅

I'd say the first one merged wins (but notify the other to propagate the change)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for this, I’ll be watching if that PR is merged before this one

) {
Comment thread
bougue-pe marked this conversation as resolved.
init {
// There must be the same number of topological boundaries and geometric boundaries
// and at least two of each (the beginning and the end)
assert(topoOffsets.size == geomOffsets.size && topoOffsets.size >= 2)
// Each list must start by 0
assert(topoOffsets[0].distance == Distance.ZERO && geomOffsets[0].distance == Distance.ZERO)
// Each list must be increasing (not strictly)
topoOffsets.zipWithNext().all { it.first <= it.second }
geomOffsets.zipWithNext().all { it.first <= it.second }
Comment on lines +70 to +71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing the assert:

Suggested change
topoOffsets.zipWithNext().all { it.first <= it.second }
geomOffsets.zipWithNext().all { it.first <= it.second }
assert(topoOffsets.zipWithNext().all { it.first <= it.second })
assert(geomOffsets.zipWithNext().all { it.first <= it.second })

}
}

val polymorphicElectrificationAdapter: PolymorphicJsonAdapterFactory<Electrification> =
PolymorphicJsonAdapterFactory.of(Electrification::class.java, "type")
.withSubtype(Electrified::class.java, "electrification")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import fr.sncf.osrd.path.interfaces.TrainPath
import fr.sncf.osrd.railjson.schema.geom.RJSLineString
import fr.sncf.osrd.sim_infra.api.NeutralSection
import fr.sncf.osrd.sim_infra.api.RawSignalingInfra
import fr.sncf.osrd.sim_infra.api.TrackSectionId
import fr.sncf.osrd.utils.DistanceRangeMap
import fr.sncf.osrd.utils.DistanceRangeMapImpl
import fr.sncf.osrd.utils.from
import fr.sncf.osrd.utils.toRangeMap
import fr.sncf.osrd.utils.units.Length
import fr.sncf.osrd.utils.units.Offset
import fr.sncf.osrd.utils.units.meters

fun makePathPropResponse(pathProperties: TrainPath, rawInfra: RawSignalingInfra): PathPropResponse {
return PathPropResponse(
Expand All @@ -21,6 +24,7 @@ fun makePathPropResponse(pathProperties: TrainPath, rawInfra: RawSignalingInfra)
makeGeographic(pathProperties),
makeOperationalPoints(pathProperties, rawInfra),
makeZones(pathProperties, rawInfra),
makeGeometricProjection(pathProperties, rawInfra),
)
}

Expand Down Expand Up @@ -148,3 +152,36 @@ private fun makeElectrificationMap(
}
return res
}

private fun makeGeometricProjection(
pathProperties: TrainPath,
rawInfra: RawSignalingInfra,
): GeometricProjection {
val trackRanges = pathProperties.getTrackRanges()

fun getTrackSectionGeometricLength(trackSection: TrackSectionId): Length<RJSLineString> {
val chunks = rawInfra.getTrackSectionChunks(trackSection)
return chunks
.map {
Length<RJSLineString>(rawInfra.getTrackChunkGeom(it).length.meters)
}
.reduce { acc, length -> acc + length.distance }
}

val geomOffsets = mutableListOf<Offset<RJSLineString>>(Offset.zero())
val topoOffsets = mutableListOf<Offset<PhysicsPath>>(Offset.zero())

trackRanges.forEachIndexed { i, range ->
val rangeTopoLength = range.length
topoOffsets.addLast(topoOffsets.last() + rangeTopoLength)

val trackSectionTopoLength = range.objectLength
val proportion = rangeTopoLength / trackSectionTopoLength.distance

val trackSectionGeomLength = getTrackSectionGeometricLength(range.value.value)
val rangeGeomLength = trackSectionGeomLength.distance * proportion
geomOffsets.addLast(geomOffsets.last() + rangeGeomLength)
Comment thread
bougue-pe marked this conversation as resolved.
}

return GeometricProjection(topoOffsets, geomOffsets)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import fr.sncf.osrd.api.RangeValues
import fr.sncf.osrd.api.path_properties.*
import fr.sncf.osrd.cli.RqFake
import fr.sncf.osrd.railjson.schema.common.graph.EdgeDirection
import fr.sncf.osrd.utils.units.Distance
import fr.sncf.osrd.utils.units.Offset
import fr.sncf.osrd.utils.units.meters
import kotlin.test.assertEquals
Expand Down Expand Up @@ -86,6 +87,25 @@ class PathPropEndpointTest : ApiTest() {
),
)
assertEquals(parsed.operationalPoints, oPs)
// Check topological distance to geometric distance projection
// The repetition of the last two values is because of a null-length range
// on the TA3 track section
val geomProjection =
GeometricProjection(
listOf(
Offset.zero(),
Offset(1_950.meters),
Offset(3_900.meters),
Offset(3_900.meters),
),
listOf(
Offset.zero(),
Offset(Distance(2464352)),
Offset(Distance(4630820)),
Offset(Distance(4630820)),
Comment on lines +103 to +105

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably not worth the previous geo distance processing, but naming the values would be easier to read/review (in case value changes, the intent is clearer):

Suggested change
Offset(Distance(2464352)),
Offset(Distance(4630820)),
Offset(Distance(4630820)),
Offset(Distance(firstTrackRangeGeoLength)),
Offset(Distance(sumTrackRangeGeoLengths)),
Offset(Distance(sumTrackRangeGeoLengths)),

),
)
assertEquals(geomProjection, parsed.geomProjection)
}

@Test
Expand Down
28 changes: 28 additions & 0 deletions editoast/core_client/src/path_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub struct PathPropertiesResponse {
pub operational_points: Vec<OperationalPointOnPath>,
/// Zones along the path
pub zones: PropertyZoneValues,
// Projection from topologic offset to geometric offset

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last one if I'm correct:

Suggested change
// Projection from topologic offset to geometric offset
// Projection from topological offset to geometric offset

pub geom_projection: GeometryProjection,
}

/// Property f64 values along a path. Each value is associated to a range of the path.
Expand Down Expand Up @@ -162,6 +164,32 @@ impl PropertyZoneValues {
}
}

/// Projection to map topological offset to geometric offset (or reversed).
/// topo_offsets and geom_offsets are the same size
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[schema(as = CorePropertyGeometryProjection)]
pub struct GeometryProjection {
Comment thread
woshilapin marked this conversation as resolved.
/// Topological offsets in millimeters.
/// Starts with 0 and is increasing.
#[schema(min_items = 2)]
topo_offsets: Vec<u64>,
/// Geometric offsets in millimeters.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Geometric offsets in millimeters.
/// Geometric offsets in millimeters, processed using Haversine formula.

(this should be propagated to openapi, which is the most important to document)

/// Starts with 0 and is increasing.
#[schema(min_items = 2)]
geom_offsets: Vec<u64>,
}
Comment thread
woshilapin marked this conversation as resolved.

impl GeometryProjection {
pub fn new(topo_offsets: Vec<u64>, geom_offsets: Vec<u64>) -> Self {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to create a fallible constructor instead: try_new?

But from what I see in the code, this constructor only exists for tests purpose. Maybe the function can also be annotated with #[cfg(test)].

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going for #[cfg(test)] was my first idea when reading this part from afar (so I'd do that at least).

assert_eq!(topo_offsets.len(), geom_offsets.len());
assert!(topo_offsets.len() >= 2);
Self {
topo_offsets,
geom_offsets,
}
}
}

impl AsCoreRequest<Json<PathPropertiesResponse>> for PathPropertiesRequest<'_> {
const URL_PATH: &'static str = "/path_properties";

Expand Down
33 changes: 33 additions & 0 deletions editoast/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions editoast/src/views/path/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use axum::extract::Json;
use axum::extract::Path;
use axum::extract::State;
use common::geometry::GeoJsonLineString;
use core_client::path_properties::GeometryProjection;
use core_client::path_properties::OperationalPointOnPath;
use core_client::path_properties::PathPropertiesRequest;
use core_client::path_properties::PropertyElectrificationValues;
Expand Down Expand Up @@ -56,6 +57,8 @@ pub(in crate::views) struct PathProperties {
/// Zones along the path
#[schema(inline)]
zones: PropertyZoneValues,
/// Curve to map topological offset to geometric offset on the path
geom_projection: GeometryProjection,
}

impl From<core_client::path_properties::PathPropertiesResponse> for PathProperties {
Expand All @@ -67,6 +70,7 @@ impl From<core_client::path_properties::PathPropertiesResponse> for PathProperti
geometry: response.geometry,
operational_points: response.operational_points,
zones: response.zones,
geom_projection: response.geom_projection,
}
}
}
Expand Down Expand Up @@ -123,6 +127,7 @@ pub(in crate::views) async fn post(
mod tests {
use axum::http::StatusCode;
use core_client::mocking::MockingClient;
use core_client::path_properties::GeometryProjection;
use core_client::path_properties::OperationalPointOnPath;
use core_client::path_properties::PropertyElectrificationValue;
use core_client::path_properties::PropertyElectrificationValues;
Expand Down Expand Up @@ -150,6 +155,7 @@ mod tests {
]])),
operational_points: vec![OperationalPointOnPath::new_test("1", 0, "1")],
zones: PropertyZoneValues::new(vec![0, 1], vec!["Zone 1".into()]),
geom_projection: GeometryProjection::new(vec![0, 1], vec![0, 0]),
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ describe('useSimulationResults', () => {
geometry: { type: 'LineString', coordinates: [] },
operational_points: [],
slopes: { boundaries: [], values: [0] },
geom_projection: { topo_offsets: [0, 1000], geom_offsets: [0, 900] },
} as unknown as PathProperties;

const preparedPathPropertiesBase = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const fetchPathProperties = async (
curves: result.curves,
electrifications: result.electrifications,
operational_points: result.operational_points,
geom_projection: result.geom_projection,
};
} catch (error) {
console.error('Error fetching path properties:', error);
Expand Down
10 changes: 10 additions & 0 deletions front/src/common/api/generatedEditoastApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3659,6 +3659,14 @@ export type InfraObjectWithGeometry = {
obj_id: string;
railjson: object;
};
export type CorePropertyGeometryProjection = {
/** Geometric offsets in millimeters.
Starts with 0 and is increasing. */
geom_offsets: number[];
/** Topological offsets in millimeters.
Starts with 0 and is increasing. */
topo_offsets: number[];
};
export type CoreOperationalPointOnPath = {
country_code: string;
/** Id of the operational point */
Expand Down Expand Up @@ -3707,6 +3715,8 @@ export type PathProperties = {
}
)[];
};
/** Curve to map topological offset to geometric offset on the path */
geom_projection: CorePropertyGeometryProjection;
/** Geometry of the path */
geometry: GeoJsonLineString;
/** Operational points along the path */
Expand Down
Loading
Loading