Skip to content
Merged
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
72 changes: 72 additions & 0 deletions Features/signale-erstellen-und-gleiskante-zuordnen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Feature: Signale erstellen und einer Gleiskante zuordnen

## Übersicht

- **Status:** Abgeschlossen
- **Autor:** Robin Radica
- **Erstellt am:** 2026-07-09
- **Letzte Änderung:** 2026-07-09

## Ziel / Problemstellung

Der Editor soll es erlauben, **Signale** als Fachdatum zu erstellen und sie einer
**Gleiskante** (`TrackEdge`) zuzuordnen. Ein Signal hat keine eigenen Koordinaten,
sondern wird per **Linear Referencing** über eine `EdgeRef` an einer Kante,
einer relativen Position (0.0 … 1.0) und einer **Seite** (Links/Rechts) verankert.
Dadurch bleibt es korrekt, wenn Knoten verschoben werden.

Bisher wurde ein Signal immer auf der rechten Seite abgelegt. Anwender:innen
sollen die Seite beim Platzieren bewusst wählen können.

## Anforderungen

### Funktionale Anforderungen

- [x] Über das Werkzeug **Signal** in der Toolbar lässt sich der Signal-Modus aktivieren.
- [x] Ein Klick nahe einer Gleiskante erzeugt ein Signal und ordnet es der
**nächstgelegenen** Kante an der angeklickten Position zu.
- [x] Klicks, die keiner Kante nahe genug sind, erzeugen kein Signal.
- [x] Die **Seite** (Links/Rechts) ist über einen Toolbar-Schalter „Signalseite“
wählbar und gilt für neu erstellte Signale (Standard: Rechts).
- [x] Die Statuszeile zeigt die aktuell gewählte Signalseite an.
- [x] Signale werden im Canvas dargestellt (Markierung + Label seitlich der Kante).

### Nicht-funktionale Anforderungen

- [x] Domänenlogik (`model`, `geometry`) bleibt frei von JavaFX-Importen und ist
unit-getestet; die Seitenwahl ist reine View-Verdrahtung über bestehende,
getestete Modelloperationen.
- [x] Architektur-Guardrails werden eingehalten (max. 3 Parameter, max. 500 LOC/Klasse).

## Nutzer / Zielgruppe

Planer:innen, die ein schematisches Gleisnetz aufbauen und Signale als Fachdaten
an Gleiskanten anbringen.

## Ein- und Ausgaben

- **Eingaben:** Werkzeug „Signal“, gewählte Signalseite (Links/Rechts),
Mausklick auf/nahe einer Gleiskante.
- **Ausgaben:** Neues `Signal` im `TrackNetwork`, referenziert über eine `EdgeRef`
(Kante + Position + Seite); Darstellung im Canvas; aktualisierte Statuszeile.

## Akzeptanzkriterien

- [x] Mit aktivem Signal-Werkzeug erzeugt ein Klick nahe einer Kante genau ein
Signal auf dieser Kante.
- [x] Ist „Links“ bzw. „Rechts“ gewählt, sitzt das neue Signal auf der jeweiligen
Seite der Kante.
- [x] Ein Klick fernab jeder Kante erzeugt kein Signal.
- [x] `mvn verify` läuft grün (Kompilierung, Tests, Static Analysis).

## Offene Fragen / Annahmen

- Umbenennen, Verschieben und Löschen bestehender Signale sowie das Zuordnen zu
einer anderen Kante sind nicht Teil dieses Features und können als Folge-Feature
ergänzt werden.

## Referenzen

- Domänenmodell: `Signal`, `EdgeRef`, `Side`, `TrackNetwork.addSignal(...)`
- Geometrie: `Geometry.nearestParam`, `Geometry.distanceToSegment`, `Geometry.offsetPoint`
- View: `NetworkEditorController` (Werkzeug- und Seitenwahl), `NetworkRenderer` (Darstellung)
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ private enum Tool { SELECT, NODE, EDGE, SIGNAL }
@FXML private ToggleButton nodeButton;
@FXML private ToggleButton edgeButton;
@FXML private ToggleButton signalButton;
@FXML private ToggleGroup sideGroup;
@FXML private ToggleButton leftSideButton;
@FXML private ToggleButton rightSideButton;
@FXML private Label statusLabel;

private TrackNetwork network = SampleNetworks.switzerland();
Expand Down Expand Up @@ -77,6 +80,11 @@ private void initialize() {
toolGroup.selectedToggleProperty().addListener((obs, old, now) -> onToolChanged(old, now));
toolGroup.selectToggle(selectButton);

leftSideButton.setUserData(Side.LEFT);
rightSideButton.setUserData(Side.RIGHT);
sideGroup.selectedToggleProperty().addListener((obs, old, now) -> onSideChanged(old, now));
sideGroup.selectToggle(rightSideButton);

canvas.setOnMousePressed(this::onMousePressed);
canvas.setOnMouseDragged(this::onMouseDragged);
canvas.setOnMouseReleased(event -> draggedNode = null);
Expand All @@ -98,6 +106,21 @@ private void onToolChanged(Toggle old, Toggle now) {
redraw();
}

private void onSideChanged(Toggle old, Toggle now) {
// Never allow a fully deselected side; keep the last choice instead.
if (now == null) {
sideGroup.selectToggle(old);
return;
}
updateStatus();
}

/** The side newly placed signals are attached to, driven by the toolbar. */
private Side selectedSide() {
Toggle selected = sideGroup.getSelectedToggle();
return selected != null ? (Side) selected.getUserData() : Side.RIGHT;
}
Comment on lines +119 to +122

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Sicherheitsverbesserung: Falls in Zukunft ein neuer Toggle-Button zur sideGroup hinzugefügt wird, bei dem vergessen wird, das userData zu setzen, oder falls getUserData() aus anderen Gründen null zurückgibt, führt der Cast (Side) selected.getUserData() zu null. Dies würde später beim Rendern in Geometry.offsetPoint zu einer NullPointerException führen. Es ist sicherer, mithilfe von Pattern Matching (instanceof Side side) zu prüfen, ob das User-Data-Objekt tatsächlich vom Typ Side ist, und andernfalls auf den Standardwert Side.RIGHT zurückzufallen.

    private Side selectedSide() {
        Toggle selected = sideGroup.getSelectedToggle();
        if (selected != null && selected.getUserData() instanceof Side side) {
            return side;
        }
        return Side.RIGHT;
    }


private void onMousePressed(MouseEvent event) {
// Ignore secondary/middle clicks so they can't create elements by accident.
if (event.getButton() != MouseButton.PRIMARY) {
Expand Down Expand Up @@ -171,7 +194,7 @@ private void handleSignalClick(double x, double y) {
}

if (closestEdge != null) {
network.addSignal(closestEdge.id(), closestParam, Side.RIGHT);
network.addSignal(closestEdge.id(), closestParam, selectedSide());
}
}

Expand Down Expand Up @@ -221,7 +244,8 @@ private void updateStatus() {
case EDGE -> pendingEdgeNode == null
? "Kante: Startknoten anklicken."
: "Kante: Zielknoten anklicken (Start: " + pendingEdgeNode.label() + ").";
case SIGNAL -> "Signal: Auf eine Gleiskante klicken, um ein Signal zu setzen.";
case SIGNAL -> "Signal: Auf eine Gleiskante klicken, um ein Signal zu setzen (Seite: "
+ (selectedSide() == Side.LEFT ? "Links" : "Rechts") + ").";
};
statusLabel.setText(String.format("%s | Knoten: %d Kanten: %d Signale: %d",
hint, network.nodes().size(), network.edges().size(), network.signals().size()));
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/com/example/vibeapp/network-editor.fxml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

<fx:define>
<ToggleGroup fx:id="toolGroup"/>
<ToggleGroup fx:id="sideGroup"/>
</fx:define>

<top>
Expand All @@ -26,6 +27,10 @@
<ToggleButton fx:id="edgeButton" text="Kante" toggleGroup="$toolGroup"/>
<ToggleButton fx:id="signalButton" text="Signal" toggleGroup="$toolGroup"/>
<Separator orientation="VERTICAL"/>
<Label text="Signalseite:"/>
<ToggleButton fx:id="leftSideButton" text="Links" toggleGroup="$sideGroup"/>
<ToggleButton fx:id="rightSideButton" text="Rechts" toggleGroup="$sideGroup"/>
<Separator orientation="VERTICAL"/>
<Button text="Beispielnetz" onAction="#onLoadSample"/>
<Button text="Leeren" onAction="#onClear"/>
</ToolBar>
Expand Down
Loading