Refactor train movement into train manager class

This commit is contained in:
Arne Keller 2020-02-21 08:19:57 +01:00
parent 4da5eeebd3
commit 122193300d
2 changed files with 378 additions and 254 deletions

View File

@ -5,15 +5,10 @@ import edu.kit.informatik.Terminal;
import edu.kit.informatik.ui.InvalidInputException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@ -29,6 +24,10 @@ public class ModelRailwaySimulation {
* Railway network used in this simulation.
*/
private final RailwayNetwork railNetwork = new RailwayNetwork();
/**
* Train manager used to simulate train placement and movement.
*/
private final TrainManager trainManager = new TrainManager(railNetwork);
/**
* List of engines used in the simulation.
*/
@ -41,10 +40,6 @@ public class ModelRailwaySimulation {
* Map of coaches used in the simulation.
*/
private final Map<Integer, Coach> coaches = new HashMap<>();
/**
* Map of trains simulated.
*/
private final Map<Integer, Train> trains = new HashMap<>();
/**
* Add a new track to the rail network of this simulation.
@ -76,7 +71,7 @@ public class ModelRailwaySimulation {
*/
public boolean removeRail(final int id) {
// check whether any trains are on this rail
return trains.values().stream().noneMatch(train -> train.isOnRail(id)) && railNetwork.removeRail(id);
return !trainManager.anyTrainOnRail(id) && railNetwork.removeRail(id);
}
/**
@ -96,7 +91,7 @@ public class ModelRailwaySimulation {
public boolean setSwitch(final int id, final Vector2D position) {
boolean success = railNetwork.setSwitch(id, position);
if (success) { // derail trains on switch, explicitly not (!) printing any removed trains (source: forum post)
trains.values().stream().filter(train -> train.isOnRail(id)).forEach(Train::removeFromRails);
trainManager.removeTrainsOnRail(id);
}
return success;
}
@ -123,9 +118,9 @@ public class ModelRailwaySimulation {
} else {
engines.sort(Comparator.comparing(Engine::getIdentifier));
for (Engine engine : engines) {
String trainId = trains.values().stream().filter(train -> train.contains(engine))
String trainId = trainManager.getTrainContainingRollingStock(engine)
.map(train -> Integer.toString(train.getIdentifier()))
.findFirst().orElse("none");
.orElse("none");
Terminal.printLine(String.format("%s %s", trainId, engine));
}
}
@ -180,9 +175,9 @@ public class ModelRailwaySimulation {
} else {
for (Integer identifier : coaches.keySet().stream().sorted().collect(Collectors.toList())) {
Coach coach = coaches.get(identifier);
String trainId = trains.values().stream().filter(train -> train.contains(coach))
String trainId = trainManager.getTrainContainingRollingStock(coach)
.map(train -> Integer.toString(train.getIdentifier()))
.findFirst().orElse("none");
.orElse("none");
Terminal.printLine(String.format("%d %s %s %d %b %b",
coach.getNumericalIdentifier(), trainId, coach.getType(),
coach.getLength(), coach.hasCouplingFront(), coach.hasCouplingBack()));
@ -213,9 +208,9 @@ public class ModelRailwaySimulation {
} else {
trainSets.sort(Comparator.comparing(TrainSet::getIdentifier));
for (TrainSet trainSet : trainSets) {
String trainId = trains.values().stream().filter(train -> train.contains(trainSet))
String trainId = trainManager.getTrainContainingRollingStock(trainSet)
.map(train -> Integer.toString(train.getIdentifier()))
.findFirst().orElse("none");
.orElse("none");
Terminal.printLine(String.format("%s %s", trainId, trainSet));
}
}
@ -228,7 +223,7 @@ public class ModelRailwaySimulation {
*/
public boolean deleteRollingStock(final String id) {
RollingStock rollingStock = getRollingStock(id);
if (trains.values().stream().anyMatch(train -> train.contains(rollingStock))) {
if (trainManager.getTrainContainingRollingStock(rollingStock).isPresent()) {
return false; // can not delete rolling stock in use
}
return engines.remove(rollingStock) || trainSets.remove(rollingStock) || coaches.values().remove(rollingStock);
@ -261,32 +256,7 @@ public class ModelRailwaySimulation {
if (rollingStock == null) {
throw new InvalidInputException("rolling stock not found");
}
if (trains.values().stream().anyMatch(train -> train.contains(rollingStock))) {
throw new InvalidInputException("rolling stock already used");
}
Train train = trains.get(trainId);
if (train != null && train.isPlaced()) {
throw new InvalidInputException("can not add rolling stock to placed train");
}
if (train != null) {
train.add(rollingStock);
} else {
int correctId = getNextTrainIdentifier();
if (trainId != correctId) {
throw new InvalidInputException("new train identifier must be next free identifier");
}
Train newTrain = new Train(trainId, rollingStock);
trains.put(trainId, newTrain);
}
}
/**
* Calculate the next train identifier.
* @return the next train identfier, or -1 if none available
*/
private int getNextTrainIdentifier() {
return IntStream.rangeClosed(1, Integer.MAX_VALUE)
.filter(id -> !trains.containsKey(id)).findFirst().orElse(-1);
trainManager.addTrain(trainId, rollingStock);
}
/**
@ -295,23 +265,14 @@ public class ModelRailwaySimulation {
* @return whether the train could be deleted
*/
public boolean deleteTrain(int id) {
Train train = trains.get(id);
if (train != null) {
trains.remove(train.getIdentifier());
return true;
}
return false;
return trainManager.deleteTrain(id);
}
/**
* Print a list of trains in the simulation.
*/
public void printTrains() {
if (trains.isEmpty()) {
Terminal.printLine("No train exists");
return;
}
trains.values().forEach(Terminal::printLine);
trainManager.printTrains();
}
/**
@ -319,12 +280,7 @@ public class ModelRailwaySimulation {
* @param id identifier of the train to show
*/
public void printTrain(int id) {
Train train = trains.get(id);
if (train != null) {
train.print();
} else {
Terminal.printError("no such train");
}
trainManager.printTrain(id);
}
/**
@ -337,168 +293,7 @@ public class ModelRailwaySimulation {
*/
public boolean putTrain(final int trainId, final Vector2D position, final Vector2D direction)
throws InvalidInputException {
Train train = trains.get(trainId);
if (train == null) {
throw new InvalidInputException("train not found");
} else if (!train.isProperTrain()) {
throw new InvalidInputException("train is not valid");
} else if (train.isPlaced()) {
throw new InvalidInputException("train is already placed");
} else if (direction.getX() != 0 && direction.getY() != 0) {
throw new InvalidInputException("invalid train direction");
} else if (!railNetwork.isReadyForTrains()) {
throw new InvalidInputException("switches not set up");
}
// attempt to place train
boolean placed = train.placeOn(railNetwork, position, direction);
// check for collisions
if (placed && !getStaticCollisions().isEmpty()) {
train.removeFromRails();
return false;
} else {
return placed;
}
}
/**
* Get collisions of trains currently placed.
* @return list of collisions (never null, sometimes empty)
*/
private List<HashSet<Train>> getStaticCollisions() {
List<HashSet<Train>> collisions = new ArrayList<>();
int maxId = trains.keySet().stream().max(Integer::compareTo).orElse(0);
for (int id1 = 1; id1 <= maxId; id1++) {
Train train1 = trains.get(id1);
if (train1 == null || !train1.isPlaced()) {
continue;
}
HashSet<Train> collision = new HashSet<>();
IntStream.rangeClosed(id1 + 1, maxId)
.mapToObj(trains::get)
.filter(Objects::nonNull)
.filter(Train::isPlaced)
.filter(train1::touches)
.forEach(collision::add);
if (!collision.isEmpty()) {
// check for existing collision
Set<Train> otherCollision = collisions.stream()
.filter(x -> x.stream().anyMatch(collision::contains))
.findFirst().orElse(null);
if (otherCollision != null) { // add to that collision
otherCollision.add(train1);
otherCollision.addAll(collision);
} else { // create a new collision
collision.add(train1);
collisions.add(collision);
}
}
}
return collisions;
}
/**
* Get collisions of moving the trains one step forward.
* @return list of collisions (never null, sometimes empty)
*/
private List<Set<Train>> getCollisionsOfOneStep() {
List<Set<Train>> collisions = new ArrayList<>();
Map<Train, Set<Rail>> occupiedRails = trains.values().stream().filter(Train::isPlaced)
.collect(Collectors.toMap(Function.identity(), Train::getOccupiedRails));
// perform step
Map<Train, Set<Rail>> nextOccupiedRails = new HashMap<>();
trains.values().stream().filter(Train::isPlaced).forEach(train -> {
Vector2D position = train.getFrontPosition();
Vector2D direction = train.getDirection();
Vector2D nextPosition = railNetwork.move(position, direction);
if (nextPosition == null
|| train.isOnPosition(nextPosition) && !train.getRearPosition().equals(train.getFrontPosition())) {
collisions.add(new HashSet<>(Arrays.asList(train)));
train.removeFromRails();
nextOccupiedRails.put(train, occupiedRails.get(train));
} else {
train.moveTo(railNetwork, nextPosition);
train.setDirection(direction);
nextOccupiedRails.put(train, train.getOccupiedRails());
}
});
checkForBlockCollisions(collisions, occupiedRails, nextOccupiedRails);
return collisions;
}
/**
* Get collisions of moving the trains one step backward.
* @return list of collisions (never null, sometimes empty)
*/
private List<Set<Train>> getCollisionsOfOneReverseStep() {
List<Set<Train>> collisions = new ArrayList<>();
Map<Train, Set<Rail>> occupiedRails = new HashMap<>();
for (Train train : trains.values()) {
if (train.isPlaced()) {
occupiedRails.put(train, train.getOccupiedRails());
}
}
// perform step
Map<Train, Set<Rail>> nextOccupiedRails = new HashMap<>();
trains.values().stream().filter(Train::isPlaced).forEach(train -> {
Vector2D front = train.getFrontPosition();
Vector2D position = train.getRearPosition();
Vector2D direction = train.getRearDirection();
Vector2D nextPosition = railNetwork.move(position, direction);
if (nextPosition == null
|| train.isOnPosition(nextPosition) && !train.getRearPosition().equals(train.getFrontPosition())) {
collisions.add(new HashSet<>(Arrays.asList(train)));
train.removeFromRails();
nextOccupiedRails.put(train, occupiedRails.get(train));
} else {
train.moveBackTo(railNetwork, nextPosition);
train.setDirection(front.subtract(train.getFrontPosition()));
nextOccupiedRails.put(train, train.getOccupiedRails());
}
});
checkForBlockCollisions(collisions, occupiedRails, nextOccupiedRails);
return collisions;
}
private void checkForBlockCollisions(List<Set<Train>> collisions, Map<Train, Set<Rail>> occupiedRails,
Map<Train, Set<Rail>> nextOccupiedRails) {
trains.values().stream().filter(train -> train.isPlaced()
&& collisions.stream().noneMatch(x -> x.contains(train))).forEach(train -> {
Set<Rail> occupiedByThisTrain = nextOccupiedRails.get(train);
trains.values().stream().filter(x -> x != train).forEach(otherTrain -> {
Set<Rail> occupiedByOtherTrainPreviously = occupiedRails.get(otherTrain);
Set<Rail> occupiedByOtherTrain = nextOccupiedRails.get(otherTrain);
if (occupiedByOtherTrain == null) {
return;
}
boolean anyIntersection = Stream.concat(
occupiedByOtherTrain.stream(), occupiedByOtherTrainPreviously.stream())
.anyMatch(occupiedByThisTrain::contains);
if (anyIntersection || train.touches(otherTrain)) {
train.removeFromRails();
otherTrain.removeFromRails();
// try to find/merge existing collisions
Set<Train> existingCollision = null;
for (Set<Train> collision : collisions) {
if (collision.contains(otherTrain) || collision.contains(train)) {
if (existingCollision == null) {
existingCollision = collision;
collision.add(train);
collision.add(otherTrain);
} else {
existingCollision.addAll(collision);
existingCollision.add(train);
existingCollision.add(otherTrain);
collisions.remove(collision);
break;
}
}
}
if (existingCollision == null) {
collisions.add(Stream.of(train, otherTrain).collect(Collectors.toSet()));
}
}
});
});
return trainManager.putTrain(trainId, position, direction);
}
/**
@ -506,36 +301,6 @@ public class ModelRailwaySimulation {
* @param speed amount of steps to move the trains
*/
public void step(final short speed) {
if (!railNetwork.isReadyForTrains()) {
Terminal.printError("rail tracks/switches not set up");
return;
}
if (trains.values().stream().noneMatch(Train::isPlaced)) {
Terminal.printLine("OK");
return;
}
List<Set<Train>> collisions = IntStream.range(0, Math.abs(speed))
.mapToObj(step -> speed >= 0 ? getCollisionsOfOneStep() : getCollisionsOfOneReverseStep())
.flatMap(List::stream).collect(Collectors.toList());
for (int id : trains.keySet().stream().sorted().collect(Collectors.toList())) {
Train train = trains.get(id);
Set<Train> collisionSet = collisions.stream()
.filter(collision -> collision.contains(train))
.findFirst().orElse(null);
if (collisionSet != null) { // print collision
int first = collisionSet.stream().min(Comparator.comparing(Train::getIdentifier)).get().getIdentifier();
if (train.getIdentifier() == first) { // only print each collision once
List<Train> collision = collisionSet.stream().sorted(Comparator.comparing(Train::getIdentifier))
.collect(Collectors.toList());
Terminal.printLine("Crash of train " + String.join(",", collision.stream()
.map(crashedTrain -> Integer.toString(crashedTrain.getIdentifier()))
.toArray(String[]::new)));
}
} else if (train.isPlaced()) { // or position, if not in collision
Terminal.printLine(String.format("Train %d at %s", train.getIdentifier(), train.getFrontPosition()));
}
}
trainManager.step(speed);
}
}

View File

@ -0,0 +1,359 @@
package edu.kit.informatik.model;
import edu.kit.informatik.Terminal;
import edu.kit.informatik.ui.InvalidInputException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
* Train manager, used for processing train placements and movement on a rail network.
*
* @author Arne Keller
* @version 1.0
*/
public final class TrainManager {
/**
* Railway network used in this simulation.
*/
private final RailwayNetwork railNetwork;
/**
* Map of trains simulated.
*/
private final Map<Integer, Train> trains = new HashMap<>();
/**
* Construct a new train manager that will operate on the provided rail network.
*
* @param railNetwork rail network to use
*/
public TrainManager(RailwayNetwork railNetwork) {
this.railNetwork = railNetwork;
}
/**
* Check whether a train is on the rail with the specified identifier. Note that a train must be partially on that
* rail, simply touching one of the end points is not enough.
*
* @param id identfier of the rail to check
* @return whether a train is on that rail
*/
public boolean anyTrainOnRail(int id) {
return trains.values().stream().anyMatch(train -> train.isOnRail(id));
}
/**
* Remove any trains on the rail with the specified identifier.
* @param id identifier of the rail
*/
public void removeTrainsOnRail(int id) {
trains.values().stream().filter(train -> train.isOnRail(id)).forEach(Train::removeFromRails);
}
/**
* Get the train containing the specified rolling stock.
*
* @param rollingStock rolling stock to search for
* @return the train containing the rolling stock
*/
public Optional<Train> getTrainContainingRollingStock(RollingStock rollingStock) {
return trains.values().stream().filter(train -> train.contains(rollingStock)).findFirst();
}
/**
* Add a rolling stock to an existing train or create a new one.
*
* @param trainId train identifier
* @param rollingStock rolling stock to add
* @throws InvalidInputException on invalid user input (e.g. rolling stock in use)
*/
public void addTrain(int trainId, RollingStock rollingStock) throws InvalidInputException {
if (getTrainContainingRollingStock(rollingStock).isPresent()) {
throw new InvalidInputException("rolling stock already used");
}
Train train = trains.get(trainId);
if (train != null && train.isPlaced()) {
throw new InvalidInputException("can not add rolling stock to placed train");
}
if (train != null) {
train.add(rollingStock);
} else {
int correctId = getNextTrainIdentifier();
if (trainId != correctId) {
throw new InvalidInputException("new train identifier must be next free identifier");
}
Train newTrain = new Train(trainId, rollingStock);
trains.put(trainId, newTrain);
}
}
/**
* Delete a train.
* @param trainId identifier of the train
* @return whether the train could be deleted
*/
public boolean deleteTrain(int trainId) {
Train train = trains.get(trainId);
if (train != null) {
trains.remove(trainId);
return true;
}
return false;
}
/**
* Print a list of trains in the simulation.
*/
public void printTrains() {
if (trains.isEmpty()) {
Terminal.printLine("No train exists");
return;
}
trains.values().forEach(Terminal::printLine);
}
/**
* Display a train as ASCII art.
* @param trainId identifier of the train to show
*/
public void printTrain(int trainId) {
Train train = trains.get(trainId);
if (train != null) {
train.print();
} else {
Terminal.printError("no such train");
}
}
/**
* Place a train on the rail network.
* @param trainId identifier of the train to place
* @param position where to place the train
* @param direction direction in which the train should initially go
* @return whether the train was successfully placed
* @throws InvalidInputException when the train is too long
*/
public boolean putTrain(int trainId, Vector2D position, Vector2D direction) throws InvalidInputException {
Train train = trains.get(trainId);
if (train == null) {
throw new InvalidInputException("train not found");
} else if (!train.isProperTrain()) {
throw new InvalidInputException("train is not valid");
} else if (train.isPlaced()) {
throw new InvalidInputException("train is already placed");
} else if (direction.getX() != 0 && direction.getY() != 0) {
throw new InvalidInputException("invalid train direction");
} else if (!railNetwork.isReadyForTrains()) {
throw new InvalidInputException("switches not set up");
}
// attempt to place train
boolean placed = train.placeOn(railNetwork, position, direction);
// check for collisions
if (placed && !getStaticCollisions().isEmpty()) {
train.removeFromRails();
return false;
} else {
return placed;
}
}
/**
* Calculate the next train identifier.
* @return the next train identfier, or -1 if none available
*/
private int getNextTrainIdentifier() {
return IntStream.rangeClosed(1, Integer.MAX_VALUE)
.filter(id -> !trains.containsKey(id)).findFirst().orElse(-1);
}
/**
* Get collisions of trains currently placed.
* @return list of collisions (never null, sometimes empty)
*/
private List<HashSet<Train>> getStaticCollisions() {
List<HashSet<Train>> collisions = new ArrayList<>();
int maxId = trains.keySet().stream().max(Integer::compareTo).orElse(0);
for (int id1 = 1; id1 <= maxId; id1++) {
Train train1 = trains.get(id1);
if (train1 == null || !train1.isPlaced()) {
continue;
}
HashSet<Train> collision = new HashSet<>();
IntStream.rangeClosed(id1 + 1, maxId)
.mapToObj(trains::get)
.filter(Objects::nonNull)
.filter(Train::isPlaced)
.filter(train1::touches)
.forEach(collision::add);
if (!collision.isEmpty()) {
// check for existing collision
Set<Train> otherCollision = collisions.stream()
.filter(x -> x.stream().anyMatch(collision::contains))
.findFirst().orElse(null);
if (otherCollision != null) { // add to that collision
otherCollision.add(train1);
otherCollision.addAll(collision);
} else { // create a new collision
collision.add(train1);
collisions.add(collision);
}
}
}
return collisions;
}
/**
* Get collisions of moving the trains one step forward.
* @return list of collisions (never null, sometimes empty)
*/
private List<Set<Train>> getCollisionsOfOneStep() {
List<Set<Train>> collisions = new ArrayList<>();
Map<Train, Set<Rail>> occupiedRails = trains.values().stream().filter(Train::isPlaced)
.collect(Collectors.toMap(Function.identity(), Train::getOccupiedRails));
// perform step
Map<Train, Set<Rail>> nextOccupiedRails = new HashMap<>();
trains.values().stream().filter(Train::isPlaced).forEach(train -> {
Vector2D position = train.getFrontPosition();
Vector2D direction = train.getDirection();
Vector2D nextPosition = railNetwork.move(position, direction);
if (nextPosition == null
|| train.isOnPosition(nextPosition) && !train.getRearPosition().equals(train.getFrontPosition())) {
collisions.add(new HashSet<>(Arrays.asList(train)));
train.removeFromRails();
nextOccupiedRails.put(train, occupiedRails.get(train));
} else {
train.moveTo(railNetwork, nextPosition);
train.setDirection(direction);
nextOccupiedRails.put(train, train.getOccupiedRails());
}
});
checkForBlockCollisions(collisions, occupiedRails, nextOccupiedRails);
return collisions;
}
/**
* Get collisions of moving the trains one step backward.
* @return list of collisions (never null, sometimes empty)
*/
private List<Set<Train>> getCollisionsOfOneReverseStep() {
List<Set<Train>> collisions = new ArrayList<>();
Map<Train, Set<Rail>> occupiedRails = new HashMap<>();
for (Train train : trains.values()) {
if (train.isPlaced()) {
occupiedRails.put(train, train.getOccupiedRails());
}
}
// perform step
Map<Train, Set<Rail>> nextOccupiedRails = new HashMap<>();
trains.values().stream().filter(Train::isPlaced).forEach(train -> {
Vector2D front = train.getFrontPosition();
Vector2D position = train.getRearPosition();
Vector2D direction = train.getRearDirection();
Vector2D nextPosition = railNetwork.move(position, direction);
if (nextPosition == null
|| train.isOnPosition(nextPosition) && !train.getRearPosition().equals(train.getFrontPosition())) {
collisions.add(new HashSet<>(Arrays.asList(train)));
train.removeFromRails();
nextOccupiedRails.put(train, occupiedRails.get(train));
} else {
train.moveBackTo(railNetwork, nextPosition);
train.setDirection(front.subtract(train.getFrontPosition()));
nextOccupiedRails.put(train, train.getOccupiedRails());
}
});
checkForBlockCollisions(collisions, occupiedRails, nextOccupiedRails);
return collisions;
}
private void checkForBlockCollisions(List<Set<Train>> collisions, Map<Train, Set<Rail>> occupiedRails,
Map<Train, Set<Rail>> nextOccupiedRails) {
trains.values().stream().filter(train -> train.isPlaced()
&& collisions.stream().noneMatch(x -> x.contains(train))).forEach(train -> {
Set<Rail> occupiedByThisTrain = nextOccupiedRails.get(train);
trains.values().stream().filter(x -> x != train).forEach(otherTrain -> {
Set<Rail> occupiedByOtherTrainPreviously = occupiedRails.get(otherTrain);
Set<Rail> occupiedByOtherTrain = nextOccupiedRails.get(otherTrain);
if (occupiedByOtherTrain == null) {
return;
}
boolean anyIntersection = Stream.concat(
occupiedByOtherTrain.stream(), occupiedByOtherTrainPreviously.stream())
.anyMatch(occupiedByThisTrain::contains);
if (anyIntersection || train.touches(otherTrain)) {
train.removeFromRails();
otherTrain.removeFromRails();
// try to find/merge existing collisions
Set<Train> existingCollision = null;
for (Set<Train> collision : collisions) {
if (collision.contains(otherTrain) || collision.contains(train)) {
if (existingCollision == null) {
existingCollision = collision;
collision.add(train);
collision.add(otherTrain);
} else {
existingCollision.addAll(collision);
existingCollision.add(train);
existingCollision.add(otherTrain);
collisions.remove(collision);
break;
}
}
}
if (existingCollision == null) {
collisions.add(Stream.of(train, otherTrain).collect(Collectors.toSet()));
}
}
});
});
}
/**
* Move the trains in this simulation, printing their new positions and any collisions.
* @param speed amount of steps to move the trains
*/
public void step(short speed) {
if (!railNetwork.isReadyForTrains()) {
Terminal.printError("rail tracks/switches not set up");
return;
}
if (trains.values().stream().noneMatch(Train::isPlaced)) {
Terminal.printLine("OK");
return;
}
List<Set<Train>> collisions = IntStream.range(0, Math.abs(speed))
.mapToObj(step -> speed >= 0 ? getCollisionsOfOneStep() : getCollisionsOfOneReverseStep())
.flatMap(List::stream).collect(Collectors.toList());
for (int id : trains.keySet().stream().sorted().collect(Collectors.toList())) {
Train train = trains.get(id);
Set<Train> collisionSet = collisions.stream()
.filter(collision -> collision.contains(train))
.findFirst().orElse(null);
if (collisionSet != null) { // print collision
int first = collisionSet.stream().min(Comparator.comparing(Train::getIdentifier)).get().getIdentifier();
if (train.getIdentifier() == first) { // only print each collision once
List<Train> collision = collisionSet.stream().sorted(Comparator.comparing(Train::getIdentifier))
.collect(Collectors.toList());
Terminal.printLine("Crash of train " + String.join(",", collision.stream()
.map(crashedTrain -> Integer.toString(crashedTrain.getIdentifier()))
.toArray(String[]::new)));
}
} else if (train.isPlaced()) { // or position, if not in collision
Terminal.printLine(String.format("Train %d at %s", train.getIdentifier(), train.getFrontPosition()));
}
}
}
}