From 7170952cb521c2a0194b6eb6f31ae0708296ac45 Mon Sep 17 00:00:00 2001
From: Stephan Richter <github@keawe.de>
Date: Sat, 26 Jun 2021 19:23:56 +0200
Subject: [PATCH] re-implemented route-search

---
 pom.xml                                       |   2 +-
 .../translations/Application.de.translation   |   2 +-
 .../de/srsoftware/web4rail/moving/Train.java  |   5 +-
 .../web4rail/threads/RoutePrepper.java        | 241 ++++++++----------
 4 files changed, 108 insertions(+), 142 deletions(-)

diff --git a/pom.xml b/pom.xml
index 0d444c4..642d572 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>de.srsoftware</groupId>
 	<artifactId>web4rail</artifactId>
-	<version>1.5.8</version>
+	<version>1.5.9</version>
 	<name>Web4Rail</name>
 	<packaging>jar</packaging>
 	<description>Java Model Railway Control</description>
diff --git a/resources/translations/Application.de.translation b/resources/translations/Application.de.translation
index 4da6c47..30033a0 100644
--- a/resources/translations/Application.de.translation
+++ b/resources/translations/Application.de.translation
@@ -460,7 +460,7 @@ TurnoutRS : WeicheRS
 TurnoutRW : WeicheRW
 Turnouts : Weichen
 turn within train : innerhalb des Zugs drehen
-Turns the train, as if it went through a loop. : Dreht den ZUg, als wenn er eine Wendeschleife passiert hätte.
+Turns the train, as if it went through a loop. : Dreht den Zug, als wenn er eine Wendeschleife passiert hätte.
 Type : Typ 
 Unknown action\: {} : Unbekannte Aktion: {}
 Unknown decoder type : Unbekannter Decoder-Typ
diff --git a/src/main/java/de/srsoftware/web4rail/moving/Train.java b/src/main/java/de/srsoftware/web4rail/moving/Train.java
index 4aa0757..abe392f 100644
--- a/src/main/java/de/srsoftware/web4rail/moving/Train.java
+++ b/src/main/java/de/srsoftware/web4rail/moving/Train.java
@@ -360,7 +360,7 @@ public class Train extends BaseClass implements Comparable<Train> {
 	}
 	
 	public Block destination(){
-		LOG.debug("{}.destination()",this);
+		//LOG.debug("{}.destination()",this);
 		if (isNull(destination)) {
 			String destTag = destinationTag();
 			LOG.debug("→ processing \"{}\"...",destTag);
@@ -985,7 +985,7 @@ public class Train extends BaseClass implements Comparable<Train> {
 			if (isSet(currentBlock)) plan.place(currentBlock);
 		}
 		
-x		if (isSet(route)) {
+		if (isSet(route)) {
 			LOG.debug("Train already on route {}",route);
 			return null;
 		}
@@ -1141,6 +1141,7 @@ x		if (isSet(route)) {
 	
 	public Context updateTrace(Context context) {
 		LOG.debug("updateTrace({})",context);
+		if (isNull(context)) return null;
 		Tile from = context.tile();
 		if (isNull(from)) from = context.contact();
 		if (isNull(from)) {
diff --git a/src/main/java/de/srsoftware/web4rail/threads/RoutePrepper.java b/src/main/java/de/srsoftware/web4rail/threads/RoutePrepper.java
index d862ad2..04f69bd 100644
--- a/src/main/java/de/srsoftware/web4rail/threads/RoutePrepper.java
+++ b/src/main/java/de/srsoftware/web4rail/threads/RoutePrepper.java
@@ -1,12 +1,11 @@
 package de.srsoftware.web4rail.threads;
 
-import java.util.HashMap;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.Map.Entry;
-import java.util.TreeMap;
-import java.util.stream.Collectors;
+import java.util.PriorityQueue;
+import java.util.Vector;
 
 import de.srsoftware.web4rail.Application;
 import de.srsoftware.web4rail.BaseClass;
@@ -19,21 +18,65 @@ import de.srsoftware.web4rail.tiles.Tile;
 
 public class RoutePrepper extends BaseClass implements Runnable{
 	
-	private static class Candidate{
+	
+
+	public static class Trail extends Vector<Route> implements Comparable<Trail> {
 
-		private int score;
-		private Route route;
+		private static final long serialVersionUID = -620911121007236751L;
+		private int score = 0;
 
-		public Candidate(Route r, int s) {
-			route = r;
-			score = s;
+		@Override
+		public int compareTo(Trail other) {
+			return other.score() - this.score();
 		}
 		
+		public Trail add(Route route, int score) {
+			add(route);
+			this.score += score;		
+			return this;
+		}
+		
+		public Collection<Trail> derive(Collection<Trail> ongoing) {
+			Vector<Trail> derived = new Vector<>();
+			for (Trail trail : ongoing) {
+				derived.add(trail.prepend(this));
+			}
+			return derived;
+		}
+		
+		public boolean endsAt(Block block) {
+			return lastElement().endBlock() == block;
+		}
+		
+		private Trail prepend(Trail trail) {
+			addAll(0, trail);
+			score += trail.score;
+			return this;
+		}
+		
+		public int score() {
+			return score;
+		}
+
 		@Override
-		public String toString() {
-			return route.toString().replace(")", ", score: "+score+")");
+		public synchronized String toString() {
+			StringBuilder sb = new StringBuilder("\n"+getClass().getSimpleName());
+			sb.append("(Score ");
+			sb.append(score);
+			sb.append(": ");
+			for (Route r : this) {
+				sb.append(r.startBlock().name);
+				sb.append(" → ");
+			}
+			if (!isEmpty())sb.append(lastElement().endBlock().name);
+			return sb+")";
 		}
+
+
+
+
 	}
+
 	
 	private Context context;
 	private Route route;
@@ -52,7 +95,7 @@ public class RoutePrepper extends BaseClass implements Runnable{
 		context = c;
 	}
 		
-	private static TreeMap<Integer,LinkedList<Route>> availableRoutes(Context c){
+	private static PriorityQueue<Trail> availableRoutes(Context c){
 		boolean error = false;
 		
 		Block startBlock = c.block();
@@ -61,14 +104,14 @@ public class RoutePrepper extends BaseClass implements Runnable{
 		Train train = c.train();
 		if (isNull(train) && (error=true)) LOG.warn("RoutePrepper.availableRoutes(…) called without a startBlock!");
 		
-		if (error) return new TreeMap<>();
+		if (error) return new PriorityQueue<>();
 		
 		Block destination = train.destination();
 		
 		Direction startDirection = c.direction();
 		LOG.debug("RoutePrepper.availableRoutes({},{},{}), dest = {}",startBlock,startDirection,train,destination);
 		
-		TreeMap<Integer, LinkedList<Route>> candidates = routesFrom(c); // map: score → [route]
+		PriorityQueue<Trail> candidates = routesFrom(c); // map: route → score
 		
 		if (isNull(destination)) {
 			LOG.debug("{} has no destination, returning {}",train,candidates);
@@ -77,114 +120,57 @@ public class RoutePrepper extends BaseClass implements Runnable{
 		
 		LOG.debug("{} is heading for {}, starting breadth-first search…",train,destination);
 		
-		HashMap<Route,Candidate> predecessors = new HashMap<>() {
-			private static final long serialVersionUID = -42682947866294566L;
-
-			public String toString() {
-				return entrySet().stream()
-					.sorted((e1,e2) -> e1.getValue().toString().compareTo(e2.getValue().toString()))
-					.map(entry -> entry.getValue()+" → "+entry.getKey())
-					.collect(Collectors.joining("\n"));
-			};
-		};
-		candidates.entrySet().stream().flatMap(entry -> entry.getValue().stream()).forEach(route -> predecessors.put(route, null));
-		TreeMap<Integer,LinkedList<Route>> routesToDest = new TreeMap<>();
-		
-		int level = 0;
-		int level2 = 0;
-		
-		while (!candidates.isEmpty()) {
-			LOG.debug("Candidates for level {}:",level);
-			candidates.entrySet().stream().flatMap(entry -> entry.getValue().stream()).forEach(route -> LOG.debug(" - {}",route));
-
-			TreeMap<Integer, LinkedList<Route>> queue = new TreeMap<>();
+		for (int depth=1; depth<99; depth++) {
+			if (candidates.stream().filter(trail -> trail.endsAt(destination)).count() > 0) return candidates; // return connectingTrails + other routes, in case all connecting trails are occupied
 			
-			while (!candidates.isEmpty()) {
-				Candidate candidate = pop(candidates);
-				LOG.debug(" - examining {}…",candidate);
-				
-				Block endBlock = candidate.route.endBlock();
-				Direction endDir = candidate.route.endDirection;
-				
-				if (endBlock == destination) {
-					LOG.debug(" - {} reaches destination!",candidate);
-					int score = candidate.score;
-					
-					// The route we found leads to the destination block.
-					// However it might be the last route in a long path.
-					// Thus, we need to get the first route in this path:					
-					while (predecessors.containsKey(candidate.route)) { 
-						Candidate predecessor = predecessors.get(candidate.route);
-						if (isNull(predecessor)) break;
-						LOG.debug("   - {} is predecessed by {}",candidate,predecessor);
-						candidate = predecessor;
-						score += candidate.score;
-					}
-					
-					LOG.debug(" → path starts with {} and has total score of {}",candidate.route,score);
-					LinkedList<Route> routesForScore = routesToDest.get(score);
-					if (isNull(routesForScore)) routesToDest.put(score, routesForScore = new LinkedList<Route>());
-					routesForScore.add(candidate.route);
-					continue;
-				}
-				
-				LOG.debug("   - {} not reaching {}, adding ongoing routes to queue:",candidate,destination);
-				TreeMap<Integer, LinkedList<Route>> successors = routesFrom(c.clone().block(endBlock).direction(endDir));
-				while (!successors.isEmpty()) {
-					int score = successors.firstKey();
-					LinkedList<Route> best = successors.remove(score);
-					score -= 25; // Nachfolgeroute
-					for (Route route : best) {
-						LOG.debug("     - queueing {} with score {}",route,score);
-						if (predecessors.containsKey(route)) {
-							LOG.debug("this route already has a predecessor: {}",predecessors.get(route));
-							continue; // Route wurde bereits besucht
-						}
-						predecessors.put(route, candidate);
-						
-						LinkedList<Route> list = queue.get(score);
-						if (isNull(list)) queue.put(score, list = new LinkedList<>());
-						list.add(route);
-					}
-				}
+			PriorityQueue<Trail> nextLevelCandidates = new PriorityQueue<Trail>();
+			for (Trail trail : candidates) {
+				Route lastRoute = trail.lastElement();
+				c.block(lastRoute.endBlock()).direction(lastRoute.endDirection);
+				PriorityQueue<Trail> ongoing = routesFrom(c);
+				if (!ongoing.isEmpty()) nextLevelCandidates.addAll(trail.derive(ongoing));
 			}
-			
-			if (!routesToDest.isEmpty()) {
-				if (level2++ > 5) return routesToDest;
-			} else LOG.debug("No routes to {} found with distance {}!",destination,level);
-			level ++;
-			candidates = queue;			
-		}
-		LOG.debug("No more candidates for routes towards {}!",destination);
+			candidates = nextLevelCandidates;
 		
-		return routesToDest;
+		}
+		return new PriorityQueue<>();
 		
 	}
 	
 	private static Route chooseRoute(Context context) {
 		LOG.debug("{}.chooseRoute({})", RoutePrepper.class.getSimpleName(), context);
-		TreeMap<Integer, LinkedList<Route>> availableRoutes = availableRoutes(context);
+		PriorityQueue<Trail> availableRoutes = availableRoutes(context);
 		LOG.debug("available routes: {}",availableRoutes);
 		while (!availableRoutes.isEmpty()) {
 			if (context.invalidated()) break;
 			LOG.debug("availableRoutes: {}", availableRoutes);
-			Entry<Integer, LinkedList<Route>> entry = availableRoutes.lastEntry();
-			List<Route> preferredRoutes = entry.getValue();
-			LOG.debug("preferredRoutes: {}", preferredRoutes);
-			Route selectedRoute = preferredRoutes.get(random.nextInt(preferredRoutes.size()));
 			
-			HashSet<Tile> stuckTrace = context.train().stuckTrace(); // if train has been stopped in between two blocks lastly: 
-                                                                     // only allow starting routes that do not conflict with current train position
-			if (isSet(stuckTrace) && !selectedRoute.path().containsAll(stuckTrace)) {
-				LOG.debug("Stuck train occupies tiles ({}) outside of {} – not allowed.", stuckTrace, selectedRoute);
-			} else if (selectedRoute.isFreeFor(context)) {
-				LOG.debug("Chose \"{}\" with priority {}.", selectedRoute, entry.getKey());
-				return selectedRoute;
+			Vector<Route> preferredRoutes = new Vector<>();
+			Integer score = null;
+			while (!availableRoutes.isEmpty()) {
+				Trail trail = availableRoutes.peek();
+				if (score == null) score = trail.score();
+				if (score != trail.score()) break;
+				availableRoutes.remove(trail);
+				preferredRoutes.add(trail.firstElement());
 			}
 
-			LOG.debug("Selected route \"{}\" is not free for {}", selectedRoute, context);
-			preferredRoutes.remove(selectedRoute);
-			if (preferredRoutes.isEmpty()) availableRoutes.remove(availableRoutes.lastKey());
+			LOG.debug("preferredRoutes: {}", preferredRoutes);
+
+			while (!preferredRoutes.isEmpty()) {
+				Route selectedRoute = preferredRoutes.remove(random.nextInt(preferredRoutes.size()));
+				HashSet<Tile> stuckTrace = context.train().stuckTrace(); // if train has been stopped in between two blocks lastly: 
+	                                                                     // only allow starting routes that do not conflict with current train position
+				if (isSet(stuckTrace) && !selectedRoute.path().containsAll(stuckTrace)) {
+					LOG.debug("Stuck train occupies tiles ({}) outside of {} – not allowed.", stuckTrace, selectedRoute);
+					continue;
+				} else if (!selectedRoute.isFreeFor(context)) {
+					LOG.debug("Selected route \"{}\" is not free for {}", selectedRoute, context);
+					continue;
+				}
+				LOG.debug("Chose \"{}\" with priority {}.", selectedRoute, score);
+				return selectedRoute;				
+			}
 		}
 		return null;
 	}
@@ -217,21 +203,6 @@ public class RoutePrepper extends BaseClass implements Runnable{
 		preparedListener = l;
 	}
 	
-	private static Candidate pop(TreeMap<Integer, LinkedList<Route>> candidates) {
-		while (!candidates.isEmpty()) {
-			int score = candidates.firstKey();
-			LinkedList<Route> list = candidates.get(score);
-			if (isNull(list) || list.isEmpty()) {
-				candidates.remove(score);
-			} else {
-				Candidate candidate = new Candidate(list.removeFirst(),score);
-				if (list.isEmpty()) candidates.remove(score);
-				return candidate;
-			}
-		}
-		return null;
-	}
-	
 	public boolean prepareRoute() {
 		if (isNull(context) || context.invalidated()) return fail();
 		route = chooseRoute(context);		
@@ -250,28 +221,28 @@ public class RoutePrepper extends BaseClass implements Runnable{
 		return route;
 	}
 
-	private static TreeMap<Integer,LinkedList<Route>> routesFrom(Context c){
+	private static PriorityQueue<Trail> routesFrom(Context context){
 		boolean error = false;
 		
-		Block startBlock = c.block();
+		Block startBlock = context.block();
 		if (isNull(startBlock) && (error=true)) LOG.warn("RoutePrepper.routesFrom(…) called without a startBlock!");
 		
-		Train train = c.train();
+		Train train = context.train();
 		if (isNull(train) && (error=true)) LOG.warn("RoutePrepper.routesFrom(…) called without a startBlock!");
 		
 		if (error) return null;
 		
 		Block destination = train.destination();
 		
-		Direction startDirection = c.direction();
+		Direction startDirection = context.direction();
 		
 		LOG.debug("     RoutePrepper.routesFrom({},{},{}), dest = {}:",startBlock,startDirection,train,destination);
 			
-		TreeMap<Integer, LinkedList<Route>> routes = new TreeMap<>();
+		PriorityQueue<Trail> trails = new PriorityQueue<>();
 		
 		for (Route route : startBlock.leavingRoutes()) {
-			int score = 0;
-
+			int score = (route.endBlock() == destination) ? 100_000 : 0;
+			
 			if (isSet(startDirection) && route.startDirection != startDirection) { // Route startet entgegen der aktuellen Fahrtrichtung des Zuges
 				if (!train.pushPull) continue; // Zug kann nicht wenden
 				if (!startBlock.turnAllowed) continue; // Wenden im Block nicht gestattet
@@ -286,16 +257,10 @@ public class RoutePrepper extends BaseClass implements Runnable{
 				LOG.debug("           …overridden by destination of train!", route, train);
 			}
 			
-			if (route.endBlock() == destination) score = 100_000;
-			
-			
-			LinkedList<Route> routesForScore = routes.get(score);
-			if (isNull(routesForScore)) routes.put(score, routesForScore = new LinkedList<Route>());
-			LOG.debug("       → candidate!");
-			routesForScore.add(route);
+			trails.add(new Trail().add(route,score));
 		}
 		
-		return routes;
+		return trails;
 	}
 	
 	@Override