Skip to content

Conversation

@Math-R
Copy link

@Math-R Math-R commented Oct 7, 2025

Description

This PR implements the collapsed nodes functionality for the Netzgrafik editor, allowing intermediate nodes to be hidden while maintaining proper visual representation of train routes.

Features Implemented

1. Node Model Enhancement

  • Added isCollapsed field to Node model with getter/setter methods
  • Integrated collapsed state into node serialization/deserialization
  • Maintains backward compatibility with existing data

2. Visual Filtering System

  • NodesView: Filter out collapsed nodes from visual display
  • ConnectionsView: Hide connections associated with collapsed nodes
  • TransitionsView: Hide transitions for collapsed nodes
  • Collapsed nodes become completely invisible in the editor

3. Intelligent Section Grouping

  • TrainrunSectionService.groupTrainrunSectionsIntoChains(): Algorithm to group sections with collapsed intermediate nodes
  • Automatically detects chains like A→B→C→D where B,C are collapsed
  • Creates grouped objects with proper start/end node references

4. Direct Path Calculation

  • TrainrunSectionsView.createViewObjectForCollapsedChain(): Creates direct visual paths A→D bypassing collapsed nodes
  • Automatic routing that circumvents collapsed intermediate nodes
  • Proper port calculation for direct connections

User Experience

  • Nodes marked as collapsed become invisible
  • Train routes A→B→C→D (with B,C collapsed) display as direct A→D connections
  • Interface remains coherent and schedules are preserved
  • No impact on existing data or workflows

Technical Implementation

  • Zero breaking changes to existing data structures
  • Clean separation between data model and visual representation
  • Proper TypeScript typing throughout

Issues

Related to feature request for collapsed nodes functionality to simplify complex network display.

Checklist

  • This PR contains a description of the changes I'm making
  • I've read the Contribution Guidelines
  • Core functionality implemented and tested: Node filtering, section grouping, path calculation working
  • I've added tests for changes or features I've introduced (comprehensive tests planned for follow-up PR)
  • I documented any high-level concepts I'm introducing in documentation/ (documentation to be added in follow-up)
  • TypeScript compilation clean: No compilation errors
  • Backward compatibility preserved: No breaking changes to existing data
  • Ready for functional testing: Core collapsed nodes feature fully operational

Next Steps (Future PRs)

  • Node creation UI for collapsed nodes
  • Legacy file compatibility (numberOfStops conversion)
  • Comprehensive unit test suite
  • User documentation and examples
  • Port assignment optimization for parallel routing

@Math-R Math-R force-pushed the mrd/modify-node-display branch 4 times, most recently from 087bc7c to 185666b Compare October 7, 2025 16:41
@Math-R Math-R marked this pull request as ready for review October 7, 2025 16:41
@Math-R Math-R requested a review from aiAdrian as a code owner October 7, 2025 16:41
@Math-R Math-R self-assigned this Oct 7, 2025
@aiAdrian
Copy link
Contributor

aiAdrian commented Oct 7, 2025

Thanks you for the new feature - would it be possible to get a live demo?

@aiAdrian
Copy link
Contributor

aiAdrian commented Oct 7, 2025

I haven't done a full review yet, but at first glance I don't understand why:

  • TrainrunSectionViewObject → there's no change trigger for isCollapsed

  • NodeViewObject → there's no change trigger for isCollapsed

It seems there's no change status (generateKey) related to isCollapsed. These objects are supposed to signal that something has changed so that a visual update can be triggered (i.e., only render what has changed to improve performance). I assume that if we don't explicitly trigger the update (mark the object as changed), it won't be updated (rendered).

  displayNodes(inputNodes: Node[]) {
    const nodes = inputNodes.filter(
      (n) =>
        this.editorView.doCullCheckPositionsInViewport([
          new Vec2D(n.getPositionX(), n.getPositionY()),
          new Vec2D(n.getPositionX() + n.getNodeWidth(), n.getPositionY()),
          new Vec2D(n.getPositionX(), n.getPositionY() + n.getNodeHeight()),
          new Vec2D(n.getPositionX() + n.getNodeWidth(), n.getPositionY() + n.getNodeHeight()),
        ]) && this.filterNodesToDisplay(n),
    );

    const group = this.nodeGroup
      .selectAll(StaticDomTags.NODE_ROOT_CONTAINER_DOM_REF)
      .data(this.createViewNodeDataObjects(nodes), (n: NodeViewObject) => n.key);

... 

(see: https://github.com/OpenRailAssociation/netzgrafik-editor-frontend/blob/main/src/app/view/editor-main-view/data-views/nodes.view.ts#L106)


It would be very helpful if we could add some tests to verify that changes to isCollapsed and it's functionality are functioning as intended.

Even if this doesn't directly affect the visualization layer, it’s still important for the underlying service methods, especially in areas like data migration. The base functionality should be properly tested to ensure robustness and maintainability.


const node: Node = this.editorView.getNodeFromConnection(con);

// filter if node is collapsed - do not show connections for collapsed nodes
Copy link

Choose a reason for hiding this comment

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

Tiny nit: why the comment here and not in transitions.view.ts?

@louisgreiner
Copy link
Contributor

Context: This PR implements part of the 3 first sections of the implementation plan given here

Copy link
Contributor

@louisgreiner louisgreiner left a comment

Choose a reason for hiding this comment

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

LGTM, not tested.

Maybe, instead of fully hiding the nodes when isCollapsed is true, you should display them as a "Dot", as it is shown here (mock-up from main issue). But maybe you've planned to do it in the next PR, ignore this comment if so.

Copy link
Member

@emersion emersion left a comment

Choose a reason for hiding this comment

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

Thanks for splitting the changes into small commits! Here are a few comments.

Copy link

@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.

Made a pass, nothing to add for now. I'll do another pass after you resolved @emersion's comments.

@Math-R Math-R force-pushed the mrd/modify-node-display branch 3 times, most recently from c4cc009 to 6145bff Compare October 16, 2025 13:06
@Math-R
Copy link
Author

Math-R commented Oct 16, 2025

I haven't done a full review yet, but at first glance I don't understand why:

  • TrainrunSectionViewObject → there's no change trigger for isCollapsed
  • NodeViewObject → there's no change trigger for isCollapsed

It seems there's no change status (generateKey) related to isCollapsed. These objects are supposed to signal that something has changed so that a visual update can be triggered (i.e., only render what has changed to improve performance). I assume that if we don't explicitly trigger the update (mark the object as changed), it won't be updated (rendered).

  displayNodes(inputNodes: Node[]) {
    const nodes = inputNodes.filter(
      (n) =>
        this.editorView.doCullCheckPositionsInViewport([
          new Vec2D(n.getPositionX(), n.getPositionY()),
          new Vec2D(n.getPositionX() + n.getNodeWidth(), n.getPositionY()),
          new Vec2D(n.getPositionX(), n.getPositionY() + n.getNodeHeight()),
          new Vec2D(n.getPositionX() + n.getNodeWidth(), n.getPositionY() + n.getNodeHeight()),
        ]) && this.filterNodesToDisplay(n),
    );

    const group = this.nodeGroup
      .selectAll(StaticDomTags.NODE_ROOT_CONTAINER_DOM_REF)
      .data(this.createViewNodeDataObjects(nodes), (n: NodeViewObject) => n.key);

... 

(see: https://github.com/OpenRailAssociation/netzgrafik-editor-frontend/blob/main/src/app/view/editor-main-view/data-views/nodes.view.ts#L106)

It would be very helpful if we could add some tests to verify that changes to isCollapsed and it's functionality are functioning as intended.

Even if this doesn't directly affect the visualization layer, it’s still important for the underlying service methods, especially in areas like data migration. The base functionality should be properly tested to ensure robustness and maintainability.

Sorry for the late answer. i'm not sure to completly understand your first point. And sure, I will add some test in the next commit

@Math-R Math-R force-pushed the mrd/modify-node-display branch 2 times, most recently from a1d2b05 to fa9b231 Compare October 16, 2025 15:03
Copy link
Member

@emersion emersion left a comment

Choose a reason for hiding this comment

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

I haven't time to read this in full yet, sorry. Here are some comments still.

As mentioned in the implementation plan, I don't think keeping a TrainrunSectionViewObject.trainrunSection is great. I think replacing it with a trainrunSections: TrainrunSection[] field would ensure we never grab information from the first trainrun when we would need to grab it from the last, or from intermediate sections. For instance the target arrival time as mentioned below in comments, but applies to others as well.

@Math-R Math-R force-pushed the mrd/modify-node-display branch from fa9b231 to 48fd522 Compare October 22, 2025 12:39
@Math-R Math-R force-pushed the mrd/modify-node-display branch from 48fd522 to 871f667 Compare October 28, 2025 11:20
emersion and others added 16 commits November 17, 2025 15:56
The TrainrunSectionViewObject already contains the full chain, no
need to re-compute it.

Signed-off-by: Simon Ser <[email protected]>
…Key()

We were only using the first trainrun section's path here.

Expose a getPath() method to get a view object's path. We need to
call it from inside generateKey() so drop the static attribute.

While at it, make the generateKey() helper function private because
it shouldn't be called from elsewhere.

Signed-off-by: Simon Ser <[email protected]>
Target arrival was included twice, and source arrival was missing.

Signed-off-by: Simon Ser <[email protected]>
We were only grabbing metadata from the first section, but we need
to make the key change when the last section changes.

Signed-off-by: Simon Ser <[email protected]>
Instead, grab the path from TrainrunSectionViewObject.getPath().

We need to update a few functions to take the veiw object instead
of the first trainrun section.

Signed-off-by: Simon Ser <[email protected]>
This function overwrites a trainrun section's path. Instead, pick
the right section (first or last) when computing path-related
outputs.

Signed-off-by: Simon Ser <[email protected]>
Further down, these two calls return the same values:

    this.getTrainrun().getTrainrunFrequency().frequency
    this.getTrainrun().getTrainrunFrequency().offset

Signed-off-by: Simon Ser <[email protected]>
…ect.generateKey()

Only the source matters for the first section, the target is handled
by the last section of the chain.

Same as this commit, but for the consecutive time:
9689208

Signed-off-by: Simon Ser <[email protected]>
case TrainrunSectionText.TrainrunSectionTravelTime:
// Special case for multiple sections: calculate total time including stop times at intermediate nodes
if (viewObject.trainrunSections.length > 1) {
const totalTime = viewObject.trainrunSections.reduce((sum, section, index) => {
Copy link
Member

Choose a reason for hiding this comment

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

We could move this into TrainrunSectionViewObject.getTravelTime(), and call it from here. That way, we fix bugs in getTravelTime() and reduce code duplication.

Copy link
Author

Choose a reason for hiding this comment

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

sure done


static getTrainrunSectionValueToShow(
trainrunSection: TrainrunSection,
trainrunSectionOrViewObject: TrainrunSection | TrainrunSectionViewObject,
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need to accept a raw TrainrunSection here: this sounds like a footgun where some callers would just pass the first section and ignore the rest. We can always take a TrainrunSectionViewObject as input (and that object may contain a single section).

Copy link
Author

Choose a reason for hiding this comment

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

Sure i made a refacto of this method

Copy link
Author

Choose a reason for hiding this comment

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

see 8ea0098

// ViewObject case: determine which section to use based on textElement
const viewObject = trainrunSectionOrViewObject;

switch (textElement) {
Copy link
Member

Choose a reason for hiding this comment

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

I think this can be merged with the switch below. We don't need to special-case multiple trainrun sections.

Copy link
Author

Choose a reason for hiding this comment

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

same as above see 8ea0098

const firstSection = viewObject.trainrunSections[0];
const lastSection = viewObject.trainrunSections.at(-1)!;

// Handle multi-section chains (collapsed nodes)
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need to special-case view objects with multiple sections. We should be able to adapt the codepath below the if block and drop the if block, for instance:

  • const trgNode = lastSection.getTargetNode();
  • const path = viewObject.getPath();

Copy link
Author

Choose a reason for hiding this comment

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

Sure changed it

static translateAndRotateText(
trainrunSection: TrainrunSection,
trainrunSectionText: TrainrunSectionText,
viewObject?: TrainrunSectionViewObject,
Copy link
Member

Choose a reason for hiding this comment

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

Can we completely replace the trainrunSection argument with the viewObject argument?

Copy link
Author

Choose a reason for hiding this comment

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

I think we can but, this changes would affect multiple functions and tests, and the main limitation is that createNumberOfStopsTextElement currently doesn’t have access to a viewObject.

Since this PR already includes quite a bit of refactoring, it might be cleaner to handle that work in a further PR.

WDYT?

Comment on lines 62 to 65
// Use viewObject path if provided, otherwise use trainrunSection path
const pathVec2D: Vec2D[] = viewObject
? viewObject.trainrunSections[0].getPath()
: trainrunSection.getPath();
Copy link
Member

Choose a reason for hiding this comment

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

Can't we use viewObject.getPath() here?

Copy link
Author

Choose a reason for hiding this comment

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

Sure, actually i think it's cleaner thanks

@emersion emersion self-requested a review November 19, 2025 13:15
@Math-R Math-R force-pushed the mrd/modify-node-display branch from f01f31b to 2ae95ab Compare November 19, 2025 16:05
…inrunSection param

-Remove TrainrunSection parameter support (accept only TrainrunSectionViewObject)

-Merge duplicate switch statements into single switch

-Merge Source/Target and Departure/Arrival into single case
-use complete path for collapsed nodes in text rotation
@Math-R Math-R requested a review from clarani November 20, 2025 08:40
Copy link
Contributor

@louisgreiner louisgreiner left a comment

Choose a reason for hiding this comment

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

Functional review

Using this scenario (no collapsed node):
reticulaire.json

And this scenario (collapsed node in Olten):
reticulaire(1).json


Copy/paste a node selection also takes the collapsed nodes -> it should not

Enregistrement.de.l.ecran.2025-11-20.142852.mp4

In the same scenario, trainruns are drawn weirdly (ports) (but may it be caused because of import/export? should try to rebase onto Valentin's branch)

image image image

It looks like the ports assignation is mixed up


Times update works great

@louisgreiner
Copy link
Contributor

The trainrun deletion using a selected trainrun then "DEL" key is broken

image

@louisgreiner
Copy link
Contributor

Comparing two network graphics:

image

The path drawn between RTR and ZF in the collapsed case is weird. It should be drawn that way (I just created a new trainrun between these 2 nodes):
image

@louisgreiner
Copy link
Contributor

There is also a small bug in the node selection:

Enregistrement.de.l.ecran.2025-11-21.135157.mp4

The workflow should be:

  • if node selection is composed of expanded and collapsed node, then keep all the nodes in the node selection
    • then if CTRL+D (duplication), duplicate everything
      • Note: there is a also a bug here, duplication does not duplicate node with the right isCollapsed value
  • if node selection is composed of only collapsed nodes, then do not push these nodes in the node selection (as it is done for filtered node selection)
    • then no action is possible

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants