You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

803 lines
23 KiB

package de.srsoftware.web4rail.moving;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.Vector;
import java.util.stream.Collectors;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.srsoftware.tools.Tag;
import de.srsoftware.web4rail.BaseClass;
import de.srsoftware.web4rail.PathFinder;
import de.srsoftware.web4rail.Plan;
import de.srsoftware.web4rail.Plan.Direction;
import de.srsoftware.web4rail.Range;
import de.srsoftware.web4rail.Route;
import de.srsoftware.web4rail.Window;
import de.srsoftware.web4rail.tags.Button;
import de.srsoftware.web4rail.tags.Checkbox;
import de.srsoftware.web4rail.tags.Fieldset;
import de.srsoftware.web4rail.tags.Form;
import de.srsoftware.web4rail.tags.Input;
import de.srsoftware.web4rail.tags.Label;
import de.srsoftware.web4rail.tags.Select;
import de.srsoftware.web4rail.tags.Table;
import de.srsoftware.web4rail.tiles.Block;
import de.srsoftware.web4rail.tiles.Contact;
import de.srsoftware.web4rail.tiles.Tile;
public class Train extends BaseClass implements Comparable<Train> {
private static final Logger LOG = LoggerFactory.getLogger(Train.class);
private static final String CAR_ID = "carId";
public static final String LOCO_ID = "locoId";
private static final String TRACE = "trace";
private static final HashMap<Id, Train> trains = new HashMap<>();
public static final String ID = "id";
private static final String NAME = "name";
private String name = null;
private static final String ROUTE = "route";
public Route route;
private static final String DIRECTION = "direction";
private Direction direction;
private static final String PUSH_PULL = "pushPull";
public boolean pushPull = false;
private static final String CARS = "cars";
private static final String LOCOS = "locomotives";
private Vector<Car> cars = new Vector<Car>();
private static final String TAGS = "tags";
private static final String DESTINATION = "destination";
private HashSet<String> tags = new HashSet<String>();
private Block currentBlock,destination = null;
LinkedList<Tile> trace = new LinkedList<Tile>();
public int speed = 0;
private Autopilot autopilot = null;
private Route nextRoute;
private class Autopilot extends Thread{
boolean stop = false;
int waitTime = 100;
@Override
public void run() {
try {
stop = false;
while (true) {
if (isNull(route)) {
Thread.sleep(waitTime);
if (waitTime > 100) waitTime /=2;
if (stop) return;
if (isNull(route)) { // may have been set by start action in between
Train.this.start();
if (isSet(destination)) Thread.sleep(1000); // limit load on PathFinder
}
} else Thread.sleep(250);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public Train(Locomotive loco) {
this(loco,null);
}
public Train(Locomotive loco, Id id) {
if (isNull(id)) id = new Id();
this.id = id;
add(loco);
trains.put(id, this);
}
public static Object action(HashMap<String, String> params, Plan plan) throws IOException {
String action = params.get(ACTION);
if (isNull(action)) return t("No action passed to Train.action!");
if (!params.containsKey(Train.ID)) {
switch (action) {
case ACTION_PROPS:
return manager();
case ACTION_ADD:
return create(params,plan);
}
return t("No train id passed!");
}
Id id = Id.from(params);
Train train = trains.get(id);
if (isNull(train)) return(t("No train with id {}!",id));
switch (action) {
case ACTION_ADD:
return train.addCar(params);
case ACTION_AUTO:
return train.automatic();
case ACTION_DROP:
return train.dropCar(params);
case ACTION_FASTER10:
return train.faster(10);
case ACTION_MOVE:
return train.setDestination(params);
case ACTION_PROPS:
return train.properties();
case ACTION_QUIT:
return train.quitAutopilot();
case ACTION_SLOWER10:
return train.slower(10);
case ACTION_START:
return train.start();
case ACTION_STOP:
return train.stopNow();
case ACTION_TURN:
return train.turn();
case ACTION_UPDATE:
return train.update(params);
}
return t("Unknown action: {}",params.get(ACTION));
}
public void addToTrace(Vector<Tile> newTiles) {
boolean active = trace.isEmpty();
for (Tile tile : newTiles) {
if (active) {
trace.addFirst(tile);
} else {
Tile dummy = trace.getFirst();
if (dummy == tile) active = true;
}
}
showTrace();
}
private Object addCar(HashMap<String, String> params) {
LOG.debug("addCar({})",params);
if (!params.containsKey(CAR_ID)) return t("No car id passed to Train.addCar!");
Car car = Car.get(params.get(CAR_ID));
if (isNull(car)) return t("No car with id \"{}\" known!",params.get(CAR_ID));
add(car);
return properties();
}
public void add(Car car) {
if (isNull(car)) return;
cars.add(car);
car.train(this);
}
public String automatic() {
if (isNull(autopilot)) {
autopilot = new Autopilot();
autopilot.start();
if (isSet(currentBlock)) plan.place(currentBlock);
}
return t("{} now in auto-mode",this);
}
public String brakeId() {
return brakeId(false);
}
public String brakeId(boolean reversed) {
TreeSet<String> carIds = new TreeSet<String>();
cars.stream().map(car -> car.id()+":"+(car.orientation == reversed ? "r":"f")).forEach(carIds::add);
String brakeId = md5sum(carIds);
LOG.debug("generated new brake id for {}: {}",this,brakeId);
return brakeId;
}
private Tag carList() {
Tag locoProp = new Tag("li").content(t("Locomotives and cars:"));
Tag carList = new Tag("ul").clazz("carlist");
for (Car car : this.cars) {
Tag li = new Tag("li");
car.link(car.name()+(car.stockId.isEmpty() ? "" : " ("+car.stockId+")")).addTo(li).content(NBSP);
car.button(t("turn within train"),Map.of(ACTION,ACTION_TURN)).addTo(li);
car.button("↑",Map.of(ACTION,ACTION_MOVE)).addTo(li);
button(t("delete"),Map.of(ACTION,ACTION_DROP,LOCO_ID,car.id().toString())).addTo(li);
li.addTo(carList);
}
List<Locomotive> locos = BaseClass.listElements(Locomotive.class).stream().filter(loco -> isNull(loco.train())).collect(Collectors.toList());
if (!locos.isEmpty()) {
Form addLocoForm = new Form("append-loco-form");
addLocoForm.content(t("add locomotive:")+"&nbsp;");
new Input(REALM, REALM_TRAIN).hideIn(addLocoForm);
new Input(ACTION, ACTION_ADD).hideIn(addLocoForm);
new Input(ID,id).hideIn(addLocoForm);
Select select = new Select(CAR_ID);
for (Car loco : locos) select.addOption(loco.id(), loco);
select.addTo(addLocoForm);
new Button(t("add"),addLocoForm).addTo(addLocoForm);
addLocoForm.addTo(new Tag("li")).addTo(carList);
}
List<Car> cars = BaseClass.listElements(Car.class).stream().filter(car -> !(car instanceof Locomotive)).filter(loco -> isNull(loco.train())).collect(Collectors.toList());
if (!cars.isEmpty()) {
Form addCarForm = new Form("append-car-form");
addCarForm.content(t("add car:")+"&nbsp;");
new Input(REALM, REALM_TRAIN).hideIn(addCarForm);
new Input(ACTION, ACTION_ADD).hideIn(addCarForm);
new Input(ID,id).hideIn(addCarForm);
Select select = new Select(CAR_ID);
for (Car car : cars) select.addOption(car.id(), car+(car.stockId.isEmpty()?"":" ("+car.stockId+")"));
select.addTo(addCarForm);
new Button(t("add"),addCarForm).addTo(addCarForm);
addCarForm.addTo(new Tag("li")).addTo(carList);
}
return carList.addTo(locoProp);
}
public List<Car> cars(){
return new Vector<Car>(cars);
}
@Override
public int compareTo(Train o) {
return name().compareTo(o.toString());
}
private static Object create(HashMap<String, String> params, Plan plan) {
Locomotive loco = (Locomotive) Locomotive.get(params.get(Train.LOCO_ID));
if (isNull(loco)) return t("unknown locomotive: {}",params.get(ID));
Train train = new Train(loco);
train.parent(plan);
if (params.containsKey(NAME)) train.name(params.get(NAME));
return train.properties();
}
public Block currentBlock() {
return currentBlock;
}
public Block destination() {
return destination;
}
public Train destination(Block dest) {
destination = dest;
return this;
}
public String directedName() {
String result = name();
if (isSet(autopilot)) result="℗"+result;
if (isNull(direction)) return result;
switch (direction) {
case NORTH:
case WEST:
return '←'+result;
case SOUTH:
case EAST:
return result+'→';
}
return result;
}
public Direction direction() {
return direction;
}
private Object dropCar(HashMap<String, String> params) {
Car car = Car.get(params.get(CAR_ID));
if (isSet(car)) {
cars.remove(car);
car.train(null);
}
return properties();
}
public void dropTrace() {
while (!trace.isEmpty()) trace.removeFirst().set(null);
}
private Tag faster(int steps) {
setSpeed(speed+steps);
return properties();
}
public static Train get(Id id) {
return trains.get(id);
}
public Train heading(Direction dir) {
direction = dir;
if (isSet(currentBlock)) plan.place(currentBlock);
return this;
}
public Tile headPos() {
return trace.getFirst();
}
public JSONObject json() {
JSONObject json = super.json();
json.put(PUSH_PULL, pushPull);
if (isSet(currentBlock)) json.put(BLOCK, currentBlock.id());
if (isSet(name))json.put(NAME, name);
if (isSet(route)) json.put(ROUTE, route.id());
if (isSet(direction)) json.put(DIRECTION, direction);
json.put(CARS,cars.stream().map(c -> c.id().toString()).collect(Collectors.toList()));
json.put(TRACE, trace.stream().map(t -> t.id().toString()).collect(Collectors.toList()));
if (!tags.isEmpty()) json.put(TAGS, tags);
return json;
}
public int length() {
int result = 0;
for (Car car : cars) result += car.length;
return result;
}
/**
* If arguments are given, the first is taken as content, the second as tag type.
* If no content is supplied, name is set as content.
* If no type is supplied, "span" is preset.
* @param args
* @return
*/
public Tag link(String...args) {
String tx = args.length<1 ? name()+NBSP : args[0];
String type = args.length<2 ? "span" : args[1];
return link(type, tx);
}
public static ArrayList<Train> list() {
ArrayList<Train> list = new ArrayList<Train>(trains.values());
list.sort((t1,t2)->t1.name.compareTo(t2.name));
return list;
}
public static void loadAll(String filename, Plan plan) throws IOException {
BufferedReader file = new BufferedReader(new FileReader(filename, UTF8));
String line = file.readLine();
while (isSet(line)) {
JSONObject json = new JSONObject(line);
Train train = new Train(null,Id.from(json));
train.load(json).parent(plan);
line = file.readLine();
}
file.close();
}
public Train load(JSONObject json) {
pushPull = json.getBoolean(PUSH_PULL);
if (json.has(DIRECTION)) direction = Direction.valueOf(json.getString(DIRECTION));
if (json.has(NAME)) name = json.getString(NAME);
if (json.has(TAGS)) json.getJSONArray(TAGS ).forEach(elem -> { tags.add(elem.toString()); });
if (json.has(TRACE)) json.getJSONArray(TRACE).forEach(elem -> { trace.add(plan.get(new Id(elem.toString()), false).set(this)); });
if (json.has(BLOCK)) currentBlock = (Block) plan.get(new Id(json.getString(BLOCK)), false).set(this); // do not move this up! during set, other fields will be referenced!
if (json.has(LOCOS)) { // for downward compatibility
for (Object id : json.getJSONArray(LOCOS)) add((Locomotive) Car.get(id));
}
for (Object id : json.getJSONArray(CARS)) add(Car.get(id));
super.load(json);
return this;
}
public static Object manager() {
Window win = new Window("train-manager", t("Train manager"));
new Tag("h4").content(t("known trains")).addTo(win);
new Tag("p").content(t("Click on a name to edit the entry.")).addTo(win);
Table table = new Table().addHead(t("Name"),t("Length"),t("Max. Speed"),t("Tags"),t("Route"),t("Current location"),t("Destination"),t("Auto pilot"));
list().forEach(train -> {
int ms = train.maxSpeed();
table.addRow(
train.link(),
train.length()+NBSP+lengthUnit,
ms == Integer.MAX_VALUE ? "–" : ms+NBSP+speedUnit,
String.join(", ", train.tags()),
train.route,
isSet(train.currentBlock) ? train.currentBlock.link() : null,
train.destination(),
t(isSet(train.autopilot)?"On":"Off")
);
});
table.addTo(win);
Form form = new Form("create-train-form");
new Input(ACTION, ACTION_ADD).hideIn(form);
new Input(REALM,REALM_TRAIN).hideIn(form);
Fieldset fieldset = new Fieldset(t("add new train"));
new Input(Train.NAME, t("new train")).addTo(new Label(t("Name:")+NBSP)).addTo(fieldset);
Select select = new Select(LOCO_ID);
for (Locomotive loco : BaseClass.listElements(Locomotive.class)) {
if (isSet(loco.train())) continue;
select.addOption(loco.id(),loco.name());
}
select.addTo(new Label(t("Locomotive:")+NBSP)).addTo(fieldset);
new Button(t("Apply"),form).addTo(fieldset);
fieldset.addTo(form).addTo(win);
return win;
}
private int maxSpeed() {
int maxSpeed = Integer.MAX_VALUE;
for (Car car : cars) {
int max = car.maxSpeed();
if (max == 0) continue;
maxSpeed = Math.min(max, maxSpeed);
}
return maxSpeed;
}
public Window moveUp(Car car) {
for (int i = 1; i<cars.size(); i++) {
if (cars.get(i) == car) {
cars.remove(i);
cars.insertElementAt(car, i-1);
break;
}
}
return properties();
}
public String name() {
return (isSet(name) ? name : cars.stream().filter(car -> isSet(car.name())).findFirst().get().name());
}
private Train name(String newName) {
this.name = newName;
return this;
}
public boolean nextRoutePrepared() {
return isSet(nextRoute) && nextRoute.state() == Route.State.PREPARED;
}
@Override
protected Window properties(List<Fieldset> preForm, FormInput formInputs, List<Fieldset> postForm) {
Fieldset otherTrainProsps = new Fieldset(t("other train properties"));
Tag propList = new Tag("ul").clazz("proplist");
carList().addTo(propList);
if (isSet(currentBlock)) currentBlock.button(currentBlock.toString()).addTo(new Tag("li").content(t("Current location:")+NBSP)).addTo(propList);
if (isSet(direction)) new Tag("li").content(t("Direction: heading {}",direction)).addTo(propList);
Tag dest = new Tag("li").content(t("Destination:")+NBSP);
if (isNull(destination)) {
new Button(t("Select from plan"),"return selectDest("+id+");").addTo(dest);
} else {
link("span",destination,Map.of(REALM,REALM_PLAN,ID,destination.id().toString(),ACTION,ACTION_CLICK)).addTo(dest);
new Button(t("Drop"),Map.of(REALM,REALM_TRAIN,ID,id,ACTION,ACTION_MOVE,DESTINATION,"")).addTo(dest);
}
dest.addTo(propList);
if (isSet(route)) route.link("li", route).addTo(propList);
int ms = maxSpeed();
if (ms < Integer.MAX_VALUE) new Tag("li").content(t("Max. Speed")+": "+maxSpeed()+NBSP+speedUnit).addTo(propList);
SortedSet<String> allTags = tags();
if (!allTags.isEmpty()) {
Tag tagList = new Tag("ul");
for (String tag : allTags) new Tag("li").content(tag).addTo(tagList);
tagList.addTo(new Tag("li").content(t("Tags"))).addTo(propList);
}
new Tag("li").content(t("length: {}",length())+NBSP+lengthUnit).addTo(propList);
if (!trace.isEmpty()) {
Tag li = new Tag("li").content(t("Occupied area:"));
Tag ul = new Tag("ul");
for (Tile tile : trace) new Tag("li").content(tile.toString()).addTo(ul);
ul.addTo(li).addTo(propList);
}
propList.addTo(otherTrainProsps);
formInputs.add(t("Name"), new Input(NAME,name));
formInputs.add(t("Push-pull train"),new Checkbox(PUSH_PULL, t("Push-pull train"), pushPull));
formInputs.add(t("Tags"), new Input(TAGS,String.join(", ", tags)));
preForm.add(Locomotive.cockpit(this));
postForm.add(otherTrainProsps);
return super.properties(preForm, formInputs, postForm);
}
public Object quitAutopilot() {
if (isSet(nextRoute)) {
nextRoute.reset();
nextRoute = null;
}
if (isSet(autopilot)) {
autopilot.stop = true;
autopilot = null;
if (isSet(currentBlock)) plan.place(currentBlock);
return t("{} stopping at next block.",this);
} else return t("autopilot not active.");
}
@Override
public void removeChild(BaseClass child) {
if (child == route) route = null;
if (child == nextRoute) nextRoute = null;
if (child == currentBlock) currentBlock = null;
if (child == destination) destination = null;
cars.remove(child);
trace.remove(child);
super.removeChild(child);
}
public void reserveNext() {
Context context = new Context(this).route(route).block(route.endBlock()).direction(route.endDirection);
Route nextRoute = PathFinder.chooseRoute(context);
if (isNull(nextRoute)) return;
nextRoute.set(context);
boolean error = !nextRoute.lockIgnoring(route);
error = error || !nextRoute.setTurnouts();
error = error || !nextRoute.fireSetupActions();
if (error) {
nextRoute.reset(); // may unlock tiles belonging to the current route.
route.lock(); // corrects unlocked tiles of nextRoute
} else {
this.nextRoute = nextRoute;
this.route.brakeCancel();
}
}
private void reverseTrace() {
LinkedList<Tile> reversed = new LinkedList<Tile>();
LOG.debug("Trace: {}",trace);
while (!trace.isEmpty()) reversed.addFirst(trace.removeFirst());
trace = reversed;
LOG.debug("reversed: {}",trace);
reversed = null;
}
public static void saveAll(String filename) throws IOException {
BufferedWriter file = new BufferedWriter(new FileWriter(filename));
for (Entry<Id, Train> entry:trains.entrySet()) {
Train train = entry.getValue();
file.write(train.json()+"\n");
}
file.close();
}
public static Select selector(Train preselected,Collection<Train> exclude) {
if (isNull(exclude)) exclude = new Vector<Train>();
Select select = new Select(Train.class.getSimpleName());
new Tag("option").attr("value","0").content(t("unset")).addTo(select);
for (Train train : Train.list()) {
if (exclude.contains(train)) continue;
Tag opt = select.addOption(train.id, train);
if (train == preselected) opt.attr("selected", "selected");
}
return select;
}
public void set(Block newBlock) {
currentBlock = newBlock;
if (isSet(currentBlock)) currentBlock.set(this);
}
private String setDestination(HashMap<String, String> params) {
String dest = params.get(DESTINATION);
if (isNull(dest)) return t("No destination supplied!");
if (dest.isEmpty()) {
destination = null;
return t("Dropped destination of {}.",this);
}
Tile tile = plan.get(new Id(dest), true);
if (isNull(tile)) return t("Tile {} not known!",dest);
if (tile instanceof Block) {
destination = (Block) tile;
automatic();
return t("{} now heading for {}",this,destination);
}
return t("{} is not a block!",tile);
}
public void setSpeed(int newSpeed) {
speed = Math.min(newSpeed,maxSpeed());
if (speed < 0) speed = 0;
cars.stream().filter(c -> c instanceof Locomotive).forEach(car -> ((Locomotive)car).setSpeed(speed));
plan.stream(t("Set {} to {} {}",this,speed,speedUnit));
}
public void setWaitTime(Range waitTime) {
if (isNull(autopilot)) return;
autopilot.waitTime = waitTime.random();
String msg = t("{} waiting {} secs...",this,autopilot.waitTime/1000d);
LOG.debug(msg);
plan.stream(msg);
}
public void showTrace() {
int remainingLength = length();
if (remainingLength<1) remainingLength=1;
for (int i=0; i<trace.size(); i++) {
Tile tile = trace.get(i);
if (remainingLength>0) {
remainingLength-=tile.length();
tile.set(this);
} else {
tile.set(null);
trace.remove(i);
i--; // do not move to next index: remove shifted the next index towards us
}
}
}
private Tag slower(int steps) {
setSpeed(speed-steps);
return properties();
}
public Object start() throws IOException {
if (isNull(currentBlock)) return t("{} not in a block",this);
if (maxSpeed() == 0) return t("Train has maximum speed of 0 {}, cannot go!",speedUnit);
if (isSet(route)) route.reset(); // reset route previously chosen
String error = null;
if (isSet(nextRoute)) {
route = nextRoute;
if (!route.lock()) return t("Was not able to lock {}",route);
nextRoute = null;
route.set(new Context(this).block(currentBlock).direction(direction));
} else {
Context context = new Context(this).block(currentBlock).direction(direction);
route = PathFinder.chooseRoute(context);
if (isNull(route)) return t("No free routes from {}",currentBlock);
if (!route.lock()) return t("Was not able to lock {}",route);
if (!route.setTurnouts()) error = t("Was not able to set all turnouts!");
context.train(this);
route.set(context);
if (isNull(error) && !route.fireSetupActions()) error = t("Was not able to fire all setup actions of route!");
}
if (direction != route.startDirection) turn();
if (isNull(error) && !route.start(this)) error = t("Was not able to assign {} to {}!",this,route);
if (isSet(error)) {
route.reset();
route = null;
return error;
}
startSimulation();
Window win = properties();
new Tag("p").content(t("Started {}",this)).addTo(win);
return win;
}
public static void startAll() {
for (Train train : list()) {
String response = train.automatic();
LOG.info(response);
}
}
private void startSimulation() {
for (Contact contact : route.contacts()) {
if (contact.addr() != 0) return; // simulate train only when all contacts are non-physical
}
try {
Thread.sleep(1000);
plan.stream(t("Simulating movement of {}...",this));
new Thread() {
public void run() {
for (Tile tile : route.path()) {
if (isNull(route)) break;
try {
if (tile instanceof Contact) {
Contact contact = (Contact) tile;
contact.activate(true);
sleep(200);
contact.activate(false);
}
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
}.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public Object stopNow() {
quitAutopilot();
setSpeed(0);
if (isSet(nextRoute)) {
nextRoute.reset();
nextRoute = null;
}
if (isSet(route)) {
route.brakeCancel();
route.reset();
route = null;
}
return properties();
}
public SortedSet<String> tags() {
TreeSet<String> list = new TreeSet<String>(tags);
//for (Locomotive loco : locos) list.addAll(loco.tags());
for (Car car:cars) list.addAll(car.tags());
return list;
}
@Override
public String toString() {
return name();
}
public Tag turn() {
LOG.debug("train.turn()");
for (Car car : cars) car.turn();
Collections.reverse(cars);
if (isSet(direction)) {
direction = direction.inverse();
reverseTrace();
}
if (isSet(currentBlock)) plan.place(currentBlock);
return properties();
}
protected Train update(HashMap<String, String> params) {
LOG.debug("update({})",params);
pushPull = params.containsKey(PUSH_PULL) && params.get(PUSH_PULL).equals("on");
if (params.containsKey(NAME)) name = params.get(NAME);
if (params.containsKey(TAGS)) {
String[] parts = params.get(TAGS).replace(",", " ").split(" ");
tags.clear();
for (String tag : parts) {
tag = tag.trim();
if (!tag.isEmpty()) tags.add(tag);
}
}
return this;
}
public boolean usesAutopilot() {
return isSet(autopilot);
}
}