Skip to content

Conversation

@sim51
Copy link
Contributor

@sim51 sim51 commented Dec 18, 2025

Displaying level crossings on maps and be able to turn on/off the layer.

Fix #14509

@github-actions github-actions bot added area:front Work on Standard OSRD Interface modules area:editoast Work on Editoast Service labels Dec 18, 2025
Signed-off-by: Benoit Simard <contact@bsimard.com>
@sim51
Copy link
Contributor Author

sim51 commented Dec 19, 2025

Import a sample data

  • Run this SQL script
INSERT INTO infra_object_level_crossing (obj_id, data, infra_id) VALUES ('PN1', '{"name": "PN1_name", "short_zone": 1000, "parts": [{"track": "TA0", "position": 700000, "pedal_upstream": 1000000, "pedal_downstream": 5000000},{"track": "TA1", "position": 500000, "pedal_upstream": 1000000, "pedal_downstream": 5000000},{"track": "TA2", "position": 500000, "pedal_upstream": 1000000, "pedal_downstream": 5000000}] }', 1);
  • Re generate the infra
$> docker exec -it osrd-editoast editoast infra generate 1 --force

Map render

At high level :
image

At a middle level :
image

At low level :
image

Question

In this example, we have one level crossing group PN1_name but on the middle screenshot we see two ...
I'm not sure that this example represent a real example.

I checked maplibre possibilities for grouping point by an attribute and it's not possible. I can't do it in JS because we use a vector layer (in opposition of a geojson layer on which is doable).

So if this result is not good enought, we need to add a new geo layer in editoast, which groups the level crossing.

@sim51 sim51 force-pushed the bsi/map-level-crossings branch from a492d1a to 3759ef8 Compare December 19, 2025 11:25
@Tguisnet
Copy link
Contributor

Normally, this situation should not occur; there will never be such a large distance between two “parts” of the PN. If this is the case, too bad, I think we can leave it as it is for now, it's fine for me !

Signed-off-by: Benoit Simard <contact@bsimard.com>
@sim51 sim51 force-pushed the bsi/map-level-crossings branch from 3759ef8 to b5b43d0 Compare December 19, 2025 11:56
@sim51 sim51 requested review from Math-R, Synar and woshilapin December 19, 2025 12:01
@sim51 sim51 marked this pull request as ready for review December 19, 2025 12:04
@sim51 sim51 requested review from a team as code owners December 19, 2025 12:04
Copy link
Contributor

@woshilapin woshilapin left a comment

Choose a reason for hiding this comment

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

Pretty straightforward review for editoast, I love those PR 😄

@flomonster
Copy link
Member

Thanks for this PR. It's really cool to see the feature come to life.

I agree with @Tguisnet this example is not realistic at all. Level crossing points should be very close.


Note

This comment is a challenge of what was decided during design workshop. So don't block the merging of this PR for that.
I did not check the latest version of the design document for the map, sorry for the late comment.

Ping @thibautsailly @Tguisnet @SharglutDev for your point of view on this feature.

UI assumption: The first icon is for a part of the level crossing and the second one for a group of level crossing (note that it was named like that in this PR).

Original desicion: We adapt the icon according to the zoom level.

My point of view: I imagined that the display of the icon would depend on whether snapping had been performed. With the little knowledge I have of maplibre I believe the API allows this easily.

Based on my point of view on this feature in the second screenshot taken by @sim51, we should have a group icon on the left and a part icon on the right. Which makes more sense (even with a weird example like this one).

@sim51
Copy link
Contributor Author

sim51 commented Dec 19, 2025

I see what you’re aiming for—this is essentially how clustering works (see this example).
However, clustering applies to an entire layer, and we can’t configure a “clustering/grouping key” to control how the aggregation is performed.

Here are the solutions I’m considering:

  • Creating one layer per level-crossing group and use the native cluster feature
  • Using a GeoJSON source so the grouping can be handled directly in JavaScript

That said, I’m not convinced this is the approach we want to take.

From my understanding of MapLibre styling, you can define:

  • Which icon to use, either statically or dynamically based on data (e.g. feature properties or zoom level)
  • Whether overlaps are allowed, along with priority rules

The styling system itself doesn’t know whether features actually overlap. Overlap handling happens at render time: MapLibre shows or hides features based on the style’s overlap rules. As a result, there is no “overlap” variable available in the styling expressions (unlike zoom, for example).

@flomonster
Copy link
Member

Note from discussion with @sim51 : it is not possible to group level crossings by object and perform clustering.

  • We do not want to create a layer for each object
  • We do not want to transfer all the geojson (not efficient enough).

A solution that could be explored at a later stage is to do the clustering on the backend with Postgis. The layer exposure framework may need to be adapted slightly.

Copy link
Contributor

@Synar Synar left a comment

Choose a reason for hiding this comment

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

Great work thanks!

Just a few minor comments ^^

import bufferStopIcon from 'assets/pictures/layersicons/bufferstop.svg';
import detectorsIcon from 'assets/pictures/layersicons/detectors.svg';
import trackSectionsIcon from 'assets/pictures/layersicons/layer_adv.svg';
import levelCorssingIcon from 'assets/pictures/layersicons/layer_level_crossing.svg';
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo ^^

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="20">
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the same svg as in the back? Why do we need some svgs in the back and some in the front? (I've never worked with our svgs so I have no clue.)

Copy link
Contributor Author

@sim51 sim51 Dec 19, 2025

Choose a reason for hiding this comment

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

For maps we need a sprite, and editoast handles this part. It takes every icons that we need on maps, and create a sprite. That's why we have the SVG on editoast and on the front (when we need it in front part, like for this one).

import OrderedLayer from '../OrderedLayer';

export function getLevelCrossingsLayerProps(params: {
sourceTable?: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense to not have a sourceTable? (Same question for other optional arguments here and below). If it's just that the value can be undefined during init, I would rather explicitly have undefined in the type than an optional parameter.

In general I believe optional arguments should be used sparingly, especially when there is no use case in mind where they should not be passed. I've fixed multiple bugs where we forgot to pass values to optional arguments which probably should not have been optional.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For highlightedArea, minZoom & maxZoom, yes it's normal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

...pick(params, ['minzoom', 'maxzoom']),
};

if (typeof params.sourceTable === 'string') res['source-layer'] = params.sourceTable;
Copy link
Contributor

Choose a reason for hiding this comment

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

If the goal is just to guard against undefined, I think typeof is a bit overkill. "!== undefined" is more in line with our current codebase.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

to be honest, we have the same code on each layer, and here it's a copy/paste.
And the sourceTable is always defined, so I'm changing it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Signed-off-by: Benoit Simard <contact@bsimard.com>
@sim51 sim51 force-pushed the bsi/map-level-crossings branch from 5c41baa to 29f27d4 Compare December 19, 2025 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:editoast Work on Editoast Service area:front Work on Standard OSRD Interface modules

Projects

None yet

Development

Successfully merging this pull request may close these issues.

front: integrate level crossings on maps

6 participants