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.
1188 lines
36 KiB
1188 lines
36 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.Collection; |
|
import java.util.Collections; |
|
import java.util.HashSet; |
|
import java.util.Iterator; |
|
import java.util.List; |
|
import java.util.Map; |
|
import java.util.SortedSet; |
|
import java.util.TreeSet; |
|
import java.util.Vector; |
|
import java.util.stream.Collectors; |
|
import java.util.stream.Stream; |
|
|
|
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.LoadCallback; |
|
import de.srsoftware.web4rail.Params; |
|
import de.srsoftware.web4rail.Plan; |
|
import de.srsoftware.web4rail.Plan.Direction; |
|
import de.srsoftware.web4rail.Route; |
|
import de.srsoftware.web4rail.functions.Function; |
|
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.tags.Window; |
|
import de.srsoftware.web4rail.threads.BrakeProcess; |
|
import de.srsoftware.web4rail.threads.DelayedExecution; |
|
import de.srsoftware.web4rail.threads.RoutePrepper; |
|
import de.srsoftware.web4rail.tiles.Block; |
|
import de.srsoftware.web4rail.tiles.Contact; |
|
import de.srsoftware.web4rail.tiles.Tile; |
|
|
|
/** |
|
* @author Stephan Richter, SRSoftware 2020-2021 * |
|
*/ |
|
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 String NAME = "name"; |
|
|
|
public static int defaultEndSpeed = 10; |
|
public static int defaultSpeedStep = 10; |
|
private String name = null; |
|
|
|
|
|
private static final String ROUTE = "route"; |
|
private Route route; |
|
|
|
private Direction direction; |
|
private boolean autopilot; |
|
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 ACTION_REVERSE = "reverse"; |
|
public static final String DESTINATION_PREFIX = "@"; |
|
public static final char TURN_FLAG = '±'; |
|
public static final char FLAG_SEPARATOR = '+'; |
|
public static final char SHUNTING_FLAG = '¥'; |
|
|
|
private HashSet<String> tags = new HashSet<String>(); |
|
|
|
private Block currentBlock,destination = null; |
|
HashSet<Tile> trace = new HashSet<Tile>(); |
|
private Vector<Block> lastBlocks = new Vector<Block>(); |
|
|
|
public int speed = 0; |
|
private static final String SHUNTING = "shunting"; |
|
|
|
public static final String SHUNT = "SHUNT"; |
|
private boolean shunting = false; |
|
private RoutePrepper routePrepper = null; |
|
|
|
private HashSet<Tile> stuckTrace = null; |
|
|
|
private Route nextPreparedRoute; |
|
|
|
private BrakeProcess brake; |
|
|
|
public static Object action(Params params, Plan plan) throws IOException { |
|
String action = params.getString(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 = BaseClass.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.start(true); |
|
case ACTION_CONNECT: |
|
return train.connect(params); |
|
case ACTION_DROP: |
|
return train.dropCar(params); |
|
case ACTION_FASTER10: |
|
return train.faster(Train.defaultSpeedStep); |
|
case ACTION_MOVE: |
|
return train.setDestination(params); |
|
case ACTION_PROPS: |
|
return train.properties(); |
|
case ACTION_QUIT: |
|
return train.properties(train.quitAutopilot()); |
|
case ACTION_REVERSE: |
|
train.quitAutopilot(); |
|
return train.reverse().properties(); |
|
case ACTION_SET_SPEED: |
|
return train.setSpeed(params.getInt(SPEED)); |
|
case ACTION_SLOWER10: |
|
return train.slower(Train.defaultSpeedStep); |
|
case ACTION_START: |
|
return train.properties(train.start(false)); |
|
case ACTION_STOP: |
|
return train.stopNow(); |
|
case ACTION_TIMES: |
|
return train.removeBrakeTimes(); |
|
case ACTION_TOGGLE_FUNCTION: |
|
return train.toggleFunction(params); |
|
case ACTION_TOGGLE_SHUNTING: |
|
train.shunting = !train.shunting; |
|
return train.properties(); |
|
case ACTION_TURN: |
|
return train.turn().properties(); |
|
case ACTION_UPDATE: |
|
return train.update(params); |
|
} |
|
String message = t("Unknown action: {}",action); |
|
return train.properties(message); |
|
} |
|
|
|
public Train add(Car car) { |
|
if (isSet(car)) { |
|
cars.add(car); |
|
car.train(this); |
|
updateEnds(); |
|
} |
|
return this; |
|
} |
|
|
|
public void addTag(String tag) { |
|
tags.add(tag); |
|
} |
|
|
|
private Object addCar(Params params) { |
|
LOG.debug("addCar({})",params); |
|
String carId = params.getString(CAR_ID); |
|
if (isNull(carId)) return t("No car id passed to Train.addCar!"); |
|
Car car = BaseClass.get(new Id(carId)); |
|
if (isNull(car)) return t("No car with id \"{}\" known!",params.getString(CAR_ID)); |
|
add(car); |
|
return properties(); |
|
} |
|
|
|
private Fieldset blockHistory() { |
|
Fieldset fieldset = new Fieldset(t("Last blocks")).id("props-history"); |
|
Tag list = new Tag("ol"); |
|
for (int i=lastBlocks.size(); i>0; i--) { |
|
lastBlocks.get(i-1).link().addTo(new Tag("li")).addTo(list); |
|
} |
|
return list.addTo(fieldset); |
|
} |
|
|
|
|
|
public String brakeId() { |
|
return brakeId(false); |
|
} |
|
|
|
public String brakeId(boolean reversed) { |
|
TreeSet<String> locoIds = new TreeSet<String>(); |
|
locos() |
|
.map(loco -> loco.id()+":"+(loco.orientation == reversed ? "r":"f")) |
|
.forEach(locoIds::add); |
|
String brakeId = md5sum(locoIds); |
|
LOG.debug("generated new {} brake id for {}: {}",reversed?"backward":"forward",this,brakeId); |
|
return brakeId; |
|
} |
|
|
|
private Fieldset brakeTimes() { |
|
Fieldset fieldset = new Fieldset(t("Brake time table")).id("props-times"); |
|
Table timeTable = new Table(); |
|
timeTable.addRow(t("forward"),t("backward"),t("Route")); |
|
List<Route> routes = BaseClass.listElements(Route.class); |
|
Collections.sort(routes, (r1,r2)->r1.name().compareTo(r2.name())); |
|
String forwardId = brakeId(false); |
|
String backwardId = brakeId(true); |
|
for (Route route: routes) { |
|
Integer forwardTime = route.brakeTime(forwardId); |
|
Integer reverseTime = route.brakeTime(backwardId); |
|
timeTable.addRow(isSet(forwardTime)?forwardTime+" ms":"-",isSet(reverseTime)?reverseTime+" ms":"-",route.name()); |
|
} |
|
|
|
timeTable.addTo(fieldset); |
|
this.button(t("Drop brake times"),Map.of(ACTION,ACTION_TIMES)).addTo(fieldset); |
|
return fieldset; |
|
} |
|
|
|
private Tag carList() { |
|
Tag locoProp = new Tag("li").content(t("Locomotives and cars")+":"); |
|
Table carList = new Table(); |
|
carList.addHead(t("Car"),t("Actions")); |
|
|
|
boolean first = true; |
|
for (Car car : cars) { |
|
Tag link = car.link(car.name()+(car.needsMaintenance()?"⚠":"")+(car.stockId.isEmpty() ? "" : " ("+car.stockId+")")); |
|
Tag buttons = new Tag("span"); |
|
|
|
car.button(t("turn within train"),Map.of(ACTION,ACTION_TURN)).addTo(buttons); |
|
if (!first) { |
|
car.button("↑",Map.of(ACTION,ACTION_MOVE)).addTo(buttons); |
|
car.button(t("decouple"),Map.of(ACTION,ACTION_DECOUPLE,REALM,REALM_CAR)).addTo(buttons); |
|
} |
|
button(t("delete"),Map.of(ACTION,ACTION_DROP,CAR_ID,car.id().toString())).addTo(buttons); |
|
carList.addRow(link,buttons); |
|
first = false; |
|
} |
|
carList.addTo(locoProp); |
|
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"); |
|
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+(loco.stockId.isEmpty()?"":" ("+loco.stockId+")")); |
|
select.addTo(addLocoForm); |
|
new Button(t("add"),addLocoForm).addTo(addLocoForm); |
|
carList.addRow(t("add locomotive"),addLocoForm); |
|
} |
|
|
|
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"); |
|
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) { |
|
String caption = null; |
|
|
|
if (!car.stockId.isEmpty()) caption = car.stockId; |
|
if (!car.tags().isEmpty()) caption = (isSet(caption) ? caption+" / " :"") + String.join(" ",car.tags()); |
|
caption = car.toString() + (isNull(caption) ? "" : " ("+caption+")"); |
|
select.addOption(car.id(), caption); |
|
} |
|
select.addTo(addCarForm); |
|
new Button(t("add"),addCarForm).addTo(addCarForm); |
|
carList.addRow(t("add car"),addCarForm); |
|
} |
|
if (isSet(currentBlock)) { |
|
Tag ul = new Tag("ul"); |
|
Train trainInBlock = isSet(currentBlock) ? currentBlock.occupyingTrain() : null; |
|
if (isSet(trainInBlock) && trainInBlock != this) trainInBlock.link().addTo(new Tag("li")).addTo(ul); |
|
for (Train tr : currentBlock.trains()) { |
|
if (tr == this) continue; |
|
Tag li = new Tag("li").addTo(ul); |
|
tr.link().addTo(li); |
|
button(t("couple"),Map.of(ACTION,ACTION_CONNECT,REALM_TRAIN,tr.id().toString())).addTo(li); |
|
} |
|
carList.addRow(t("other trains in {}",currentBlock),ul); |
|
|
|
} |
|
|
|
return locoProp; |
|
|
|
} |
|
|
|
public List<Car> cars(){ |
|
return new Vector<Car>(cars); |
|
} |
|
|
|
@Override |
|
public int compareTo(Train o) { |
|
return name().compareTo(o.toString()); |
|
} |
|
|
|
public Window connect(Params params) { |
|
Train other = BaseClass.get(new Id(params.getString(REALM_TRAIN))); |
|
if (isSet(other)) coupleWith(other, false); |
|
return properties(); |
|
} |
|
|
|
public Context contact(Contact contact) { |
|
if (isNull(route)) return new Context(contact).train(this); |
|
return updateTrace(route.contact(contact)); |
|
} |
|
|
|
|
|
public void coupleWith(Train parkingTrain,boolean swap) { |
|
if (isSet(direction) && isSet(parkingTrain.direction) && parkingTrain.direction != direction) parkingTrain.turn(); |
|
if (swap) { |
|
Vector<Car> dummy = new Vector<Car>(); |
|
for (Car car : parkingTrain.cars) dummy.add(car.train(this)); |
|
dummy.addAll(cars); |
|
cars = dummy; |
|
} else { |
|
for (Car car : parkingTrain.cars) { |
|
cars.add(car.train(this)); |
|
} |
|
} |
|
|
|
parkingTrain.remove(); |
|
if (isSet(currentBlock)) currentBlock.setTrain(this); |
|
} |
|
|
|
private static Object create(Params params, Plan plan) { |
|
String locoId = params.getString(Train.LOCO_ID); |
|
if (isNull(locoId)) return t("Need loco id to create new train!"); |
|
Locomotive loco = BaseClass.get(new Id(locoId)); |
|
if (isNull(loco)) return t("unknown locomotive: {}",params.getString(ID)); |
|
Train train = new Train().add(loco); |
|
train.parent(plan); |
|
if (params.containsKey(NAME)) train.name(params.getString(NAME)); |
|
train.register(); |
|
return train.properties(); |
|
} |
|
|
|
public Block currentBlock() { |
|
return currentBlock; |
|
} |
|
|
|
|
|
public Object decoupleAfter(Car car) { |
|
for (int i=0; i<cars.size();i++) { |
|
if (car == cars.get(i) && splitAfter(i)) break; |
|
} |
|
return properties(); |
|
} |
|
|
|
public Block destination(){ |
|
LOG.debug("{}.destination()",this); |
|
if (isNull(destination)) { |
|
String destTag = destinationTag(); |
|
LOG.debug("→ processing \"{}\"...",destTag); |
|
if (isSet(destTag)) { |
|
destTag = destTag.split(DESTINATION_PREFIX)[1]; |
|
LOG.debug("....processing \"{}\"…",destTag); |
|
for (int i=destTag.length()-1; i>0; i--) { |
|
switch (destTag.charAt(i)) { |
|
case FLAG_SEPARATOR: |
|
destTag = destTag.substring(0,i); |
|
i=0; |
|
break; |
|
case SHUNTING_FLAG: |
|
LOG.debug("....enabled shunting option"); |
|
shunting = true; |
|
break; |
|
} |
|
} |
|
destination = BaseClass.get(new Id(destTag)); |
|
} |
|
}// else LOG.debug("→ heading towards {}",destination); |
|
return destination; |
|
} |
|
|
|
public String destinationTag() { |
|
for (String tag : tags()) { // check, if endBlock is in train's destinations |
|
if (tag.startsWith(DESTINATION_PREFIX)) return tag; |
|
} |
|
return null; |
|
} |
|
|
|
public String directedName() { |
|
String result = name(); |
|
if (needsMainenance()) result+="⚠"; |
|
String mark = autopilot ? "ⓐ" : ""; |
|
if (isNull(direction)) return result; |
|
switch (direction) { |
|
case NORTH: |
|
case WEST: |
|
return '←'+mark+result; |
|
case SOUTH: |
|
case EAST: |
|
return result+mark+'→'; |
|
} |
|
return mark+result; |
|
} |
|
|
|
public Direction direction() { |
|
return direction; |
|
} |
|
|
|
private Object dropCar(Params params) { |
|
String carId = params.getString(CAR_ID); |
|
if (isNull(carId)) return t("Cannot drop car without car id!"); |
|
Car car = BaseClass.get(new Id(carId)); |
|
if (isSet(car)) { |
|
cars.remove(car); |
|
car.train(null); |
|
} |
|
if (cars.isEmpty()) { |
|
remove(); |
|
return t("Removed train \"{}\"",this); |
|
} |
|
return properties(); |
|
} |
|
|
|
public boolean drop(Route oldRoute) { |
|
if (isNull(route)) return true; |
|
if (route != oldRoute) return false; |
|
route = null; |
|
return true; |
|
} |
|
|
|
|
|
public Train dropTrace(boolean dropStuck) { |
|
while (!trace.isEmpty()) trace.stream().findFirst().get().free(this); |
|
trace.clear(); |
|
if (dropStuck) stuckTrace = null; |
|
return this; |
|
} |
|
|
|
private BrakeProcess endBrake() { |
|
if (isNull(brake)) return null; |
|
try { |
|
return brake.end(); |
|
} finally { |
|
brake = null; |
|
} |
|
} |
|
|
|
public void endRoute(Route endedRoute) { |
|
LOG.debug("{}.endRoute({})",this,endedRoute); |
|
BrakeProcess brake = endBrake(); |
|
direction = endedRoute.endDirection; // muss vor der Auswertung des Destination-Tags stehen! |
|
Block endBlock = endedRoute.endBlock(); |
|
Block startBlock = endedRoute.startBlock(); |
|
if (endBlock == destination) { |
|
destination = null; |
|
|
|
String destTag = destinationTag(); |
|
if (isSet(destTag)) { |
|
LOG.debug("destination list: {}",destTag); |
|
String[] parts = destTag.split(Train.DESTINATION_PREFIX); |
|
for (int i=0; i<parts.length;i++) LOG.debug(" part {}: {}",i+1,parts[i]); |
|
String destId = parts[1]; |
|
LOG.debug("destination tag: {}",destId); |
|
boolean turn = false; |
|
|
|
for (int i=destId.length()-1; i>0; i--) { |
|
switch (destId.charAt(i)) { |
|
case Train.FLAG_SEPARATOR: |
|
destId = destId.substring(0,i); |
|
i=0; |
|
break; |
|
case Train.TURN_FLAG: |
|
turn = true; |
|
LOG.debug("Turn flag is set!"); |
|
break; |
|
} |
|
} |
|
if (destId.equals(endBlock.id().toString())) { |
|
if (turn) turn(); |
|
|
|
// update destination tag: remove and add altered tag: |
|
removeTag(destTag); |
|
destTag = destTag.substring(parts[1].length()+1); |
|
if (destTag.isEmpty()) { // no further destinations |
|
destTag = null; |
|
} else addTag(destTag); |
|
} |
|
} |
|
|
|
if (isNull(destTag)) { |
|
quitAutopilot(); |
|
plan.stream(t("{} reached it`s destination!",this)); |
|
} |
|
|
|
|
|
} |
|
if (isSet(brake)) brake.updateTime(); |
|
Integer waitTime = route.waitTime(); |
|
nextPreparedRoute = route.dropNextPreparedRoute(); |
|
if (isSet(nextPreparedRoute)) LOG.debug("nextPreparedRoute is now {}",nextPreparedRoute); |
|
if ((!autopilot) || isNull(nextPreparedRoute) || (isSet(waitTime) && waitTime > 0)) setSpeed(0); |
|
route = null; |
|
endBlock.setTrain(this); |
|
shunting = false; // wird in setTrain verwendet, muss also danach stehen |
|
currentBlock = endBlock; |
|
trace.add(endBlock); |
|
if (!trace.contains(startBlock)) startBlock.dropTrain(this); |
|
stuckTrace = null; |
|
if (autopilot) { |
|
if (isNull(waitTime)) waitTime = 0; |
|
if (waitTime>0) plan.stream(t("{} waiting {} secs",this,(int)(waitTime/1000))); |
|
new DelayedExecution(waitTime,this) { |
|
|
|
@Override |
|
public void execute() { |
|
if (autopilot) Train.this.start(false); |
|
} |
|
}; |
|
} |
|
|
|
long len = endedRoute.length(); |
|
if (len>0) cars.forEach(car -> car.addDistance(len)); |
|
} |
|
|
|
private Tag faster(int steps) { |
|
return setSpeed(speed+steps); |
|
} |
|
|
|
public boolean functionEnabled(String name) { |
|
return locos().flatMap( |
|
loco -> loco.functions().stream().filter(f -> f.enabled() && f.name().equals(name)) |
|
).findAny().isPresent(); |
|
} |
|
|
|
public Stream<String> functionNames() { |
|
return locos().flatMap(Locomotive::functionNames).distinct(); |
|
} |
|
|
|
private boolean hasLoco() { |
|
return locos().count() > 0; |
|
} |
|
|
|
public boolean hasNextPreparedRoute() { |
|
return isSet(nextPreparedRoute) || (isSet(route) && isSet(route.getNextPreparedRoute())); |
|
} |
|
|
|
|
|
public Train heading(Direction dir) { |
|
LOG.debug("{}.heading({})",this,dir); |
|
direction = dir; |
|
if (isSet(currentBlock)) plan.place(currentBlock); |
|
return this; |
|
} |
|
|
|
public boolean isShunting() { |
|
return shunting; |
|
} |
|
|
|
public boolean isStoppable() { |
|
if (speed > 0) return true; |
|
if (isSet(routePrepper)) return true; |
|
if (isSet(route)) return true; |
|
return false; |
|
} |
|
|
|
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 Collection<Block> lastBlocks(int count) { |
|
Vector<Block> blocks = new Vector<Block>(count); |
|
for (int i=0; i<count && i<lastBlocks.size(); i++) blocks.add(lastBlocks.get(i)); |
|
return blocks; |
|
} |
|
|
|
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 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); |
|
|
|
new 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(LOCOS)) { // for downward compatibility |
|
for (Object id : json.getJSONArray(LOCOS)) add(BaseClass.get(new Id(""+id))); |
|
} |
|
for (Object id : json.getJSONArray(CARS)) add(BaseClass.get(new Id(""+id))); |
|
new LoadCallback() { |
|
@Override |
|
public void afterLoad() { |
|
if (json.has(TRACE)) json.getJSONArray(TRACE).forEach(elem -> { |
|
Tile tile = plan.get(new Id(elem.toString()), false); |
|
tile.setTrain(Train.this); |
|
trace.add(tile); |
|
}); |
|
if (json.has(BLOCK)) {// do not move this up! during set, other fields will be referenced! |
|
currentBlock = (Block) plan.get(Id.from(json, BLOCK), false); |
|
if (isSet(currentBlock)) { |
|
currentBlock.setTrain(Train.this); |
|
trace.add(currentBlock); |
|
// currentBlock.add(Train.this, direction); |
|
} |
|
} |
|
} |
|
}; |
|
super.load(json); |
|
return this; |
|
} |
|
|
|
private Stream<Locomotive> locos() { |
|
return cars.stream().filter(c -> c instanceof Locomotive).map(c -> (Locomotive)c); |
|
} |
|
|
|
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("Maximum Speed"),t("Tags"),t("Route"),t("Current location"),t("Destination"),t("Auto pilot")); |
|
BaseClass.listElements(Train.class).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, |
|
null, // TODO: show destination here! |
|
null // TODO: show state of autopilot here |
|
); |
|
}); |
|
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")+COL)).addTo(fieldset); |
|
|
|
Select select = new Select(LOCO_ID); |
|
BaseClass.listElements(Locomotive.class) |
|
.stream() |
|
.filter(loco -> isNull(loco.train())) |
|
.sorted((l1,l2)->l1.name().compareTo(l2.name())) |
|
.forEach(loco -> select.addOption(loco.id(),loco.name())); |
|
select.addTo(new Label(t("Locomotive")+COL)).addTo(fieldset); |
|
|
|
new Button(t("Apply"),form).addTo(fieldset); |
|
fieldset.addTo(form).addTo(win); |
|
|
|
return win; |
|
} |
|
|
|
|
|
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() { |
|
if (isSet(name)) return name; |
|
if (cars.isEmpty()) return t("emtpy train"); |
|
for (Car car : cars) { |
|
String name = car.name(); |
|
if (isSet(name)) return name; |
|
} |
|
return t("empty train"); |
|
} |
|
|
|
private Train name(String newName) { |
|
this.name = newName; |
|
return this; |
|
} |
|
|
|
private boolean needsMainenance() { |
|
for (Car car: cars) { |
|
if (car.needsMaintenance()) return true; |
|
} |
|
return false; |
|
} |
|
|
|
public boolean onTrace(Tile t) { |
|
return trace.contains(t); |
|
} |
|
|
|
|
|
@Override |
|
protected Window properties(List<Fieldset> preForm, FormInput formInputs, List<Fieldset> postForm,String...errors) { |
|
Tag propList = new Tag("ul").clazz("proplist"); |
|
|
|
if (isSet(currentBlock)) currentBlock.button(currentBlock.toString()).addTo(new Tag("li").content(t("Current location")+COL)).addTo(propList); |
|
Tag directionLi = null; |
|
if (isSet(direction)) directionLi = new Tag("li").content(t("Direction: heading {}",direction)+NBSP); |
|
if (isNull(directionLi)) directionLi = new Tag("li"); |
|
button(t("reverse train"), Map.of(ACTION,ACTION_REVERSE)).title(t("Turns the train, as if it went through a loop.")).addTo(directionLi).addTo(propList); |
|
|
|
Tag dest = new Tag("li").content(t("Destination")+COL); |
|
if (isSet(destination)) { |
|
link("span",destination,Map.of(REALM,REALM_PLAN,ID,destination.id().toString(),ACTION,ACTION_CLICK),null).addTo(dest); |
|
new Button(t("Drop"),Map.of(REALM,REALM_TRAIN,ID,id,ACTION,ACTION_MOVE,DESTINATION,"")).addTo(dest); |
|
} |
|
button(t("Select from plan"),Map.of(ACTION,ACTION_MOVE,ASSIGN,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("Maximum Speed")+COL+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")+COL); |
|
Tag ul = new Tag("ul"); |
|
for (Tile tile : trace) new Tag("li").content(tile.toString()).addTo(ul); |
|
ul.addTo(li).addTo(propList); |
|
} |
|
carList().addTo(propList); |
|
|
|
|
|
formInputs.add(t("Name"), new Input(NAME,name())); |
|
formInputs.add(t("Shunting"),new Checkbox(SHUNTING, t("train is shunting"), shunting)); |
|
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))); |
|
|
|
if (this.hasLoco()) preForm.add(Locomotive.cockpit(this)); |
|
postForm.add(propList.addTo(new Fieldset(t("other train properties")+(needsMainenance()?NBSP+"⚠":"")).id("props-other"))); |
|
postForm.add(brakeTimes()); |
|
postForm.add(blockHistory()); |
|
|
|
|
|
return super.properties(preForm, formInputs, postForm,errors); |
|
} |
|
|
|
public String quitAutopilot() { |
|
if (isSet(routePrepper)) { |
|
routePrepper.stop(); |
|
routePrepper = null; |
|
} |
|
if (autopilot) { |
|
autopilot = false; |
|
if (isSet(currentBlock)) plan.place(currentBlock); |
|
} |
|
return null; |
|
} |
|
|
|
@Override |
|
public BaseClass remove() { |
|
if (isSet(currentBlock)) currentBlock.removeChild(this); |
|
if (isSet(route)) route.removeChild(this); |
|
for (Tile t:trace) t.removeChild(this); |
|
return super.remove(); |
|
} |
|
|
|
private Window removeBrakeTimes() { |
|
List<Route> routes = BaseClass.listElements(Route.class); |
|
for (Route route: routes) route.dropBraketimes(brakeId(false),brakeId(true)); |
|
return properties(); |
|
} |
|
|
|
@Override |
|
public void removeChild(BaseClass child) { |
|
LOG.debug("{}.removeChild({})",this,child); |
|
if (child == route) route = null; |
|
//if (child == nextRoute) nextRoute = null; // TODO |
|
if (child == currentBlock) currentBlock = null; |
|
if (child == destination) destination = null; |
|
if (child == routePrepper) routePrepper.stop(); |
|
cars.remove(child); |
|
trace.remove(child); |
|
super.removeChild(child); |
|
} |
|
|
|
public Iterator<String> removeTag(String tag) { |
|
tags.remove(tag); |
|
return tags().iterator(); |
|
} |
|
|
|
/** |
|
* This turns the train as if it went through a loop. Example: |
|
* before: CabCar→ MiddleCar→ Loco→ |
|
* after: ←Loco ←MiddleCar ←CabCar |
|
*/ |
|
public Train reverse() { |
|
LOG.debug("train.reverse();"); |
|
|
|
if (isSet(direction)) direction = direction.inverse(); |
|
if (isSet(currentBlock)) { |
|
if (isNull(direction)) direction = currentBlock.directionA(); |
|
plan.place(currentBlock); |
|
} |
|
return this; |
|
} |
|
|
|
public Route route() { |
|
return route; |
|
} |
|
|
|
public static void saveAll(String filename) throws IOException { |
|
BufferedWriter file = new BufferedWriter(new FileWriter(filename)); |
|
for (Train train:BaseClass.listElements(Train.class)) 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); |
|
|
|
List<Train> trains = BaseClass.listElements(Train.class); |
|
trains.sort((t1,t2)->t1.name().compareTo(t2.name())); |
|
for (Train train : trains) { |
|
if (exclude.contains(train)) continue; |
|
Tag opt = select.addOption(train.id, train); |
|
if (train == preselected) opt.attr("selected", "selected"); |
|
} |
|
return select; |
|
} |
|
|
|
public Train set(Block newBlock) { |
|
LOG.debug("{}.set({})",this,newBlock); |
|
if (isSet(currentBlock)) { |
|
if (newBlock == currentBlock) { |
|
if (currentBlock.occupyingTrain() != this) currentBlock.setTrain(this); |
|
return this; |
|
} |
|
currentBlock.free(this); |
|
} |
|
currentBlock = newBlock; |
|
if (isSet(currentBlock)) { |
|
currentBlock.setTrain(this); |
|
lastBlocks.add(newBlock); |
|
if (lastBlocks.size()>32) lastBlocks.remove(0); |
|
} |
|
return this; |
|
} |
|
|
|
private Object setDestination(Params params) { |
|
String dest = params.getString(DESTINATION); |
|
if (isNull(currentBlock)) return properties("{} is not in a block!"); |
|
if (isNull(dest)) return properties(t("No destination supplied!")); |
|
if (dest.isEmpty()) { |
|
destination = null; |
|
return properties(); |
|
} |
|
Tile tile = plan.get(new Id(dest), true); |
|
if (isNull(tile)) return properties(t("Tile {} not known!",dest)); |
|
if (tile instanceof Block) { |
|
if (shunting) { |
|
boolean connection = currentBlock.routes().stream().anyMatch(route -> route.startBlock() == currentBlock && route.endBlock() == tile); |
|
if (!connection) return t("No direct route from {} to {}",currentBlock,tile); |
|
} |
|
|
|
destination = (Block) tile; |
|
start(true); |
|
return t("{} now heading for {}",this,destination); |
|
} |
|
return properties(t("{} is not a block!",tile)); |
|
} |
|
|
|
public Object setFunction(int num, boolean active) { |
|
// TODO |
|
return properties(); |
|
} |
|
|
|
public Train setRoute(Route newRoute) { |
|
route = newRoute; |
|
return this; |
|
} |
|
|
|
public Tag setSpeed(int newSpeed) { |
|
LOG.debug("{}.setSpeed({})",this,newSpeed); |
|
speed = Math.min(newSpeed,maxSpeed()); |
|
if (speed < 0) speed = 0; |
|
locos().forEach(loco -> loco.setSpeed(speed)); |
|
plan.stream(t("Set {} to {} {}",this,speed,speedUnit)); |
|
return properties(); |
|
} |
|
|
|
private Tag slower(int steps) { |
|
return setSpeed(speed-steps); |
|
} |
|
|
|
public boolean splitAfter(int position) { |
|
if (isNull(currentBlock)) return false; // can only split within blocks! |
|
Train remaining = new Train(); |
|
int len = cars.size(); |
|
for (int i=0; i<len; i++) { |
|
if (i>=position) { |
|
Car car = cars.remove(position); |
|
LOG.debug("Moving {} from {} to {}",car,this,remaining); |
|
remaining.add(car); |
|
if (isNull(remaining.name)) { |
|
remaining.name = car.name(); |
|
} else if (remaining.name.length()+car.name().length()<30){ |
|
remaining.name += ", "+car.name(); |
|
} |
|
} else LOG.debug("Skipping {}",cars.get(i)); |
|
} |
|
if (remaining.cars.isEmpty()) return false; |
|
remaining.direction = this.direction; |
|
this.name = null; |
|
currentBlock.addParkedTrain(remaining); |
|
remaining.currentBlock = currentBlock; |
|
plan.place(currentBlock); |
|
return true; |
|
} |
|
|
|
|
|
public String start(boolean auto) { |
|
LOG.debug("{}.start({})",this,auto?"auto":""); |
|
if (auto != autopilot) { |
|
autopilot |= auto; |
|
if (isSet(currentBlock)) plan.place(currentBlock); |
|
} |
|
|
|
if (isSet(nextPreparedRoute)) { |
|
LOG.debug("starting nextPreparedRoute: {}",nextPreparedRoute); |
|
if (nextPreparedRoute.startNow()) { |
|
LOG.debug("dropped nextPreparedRoute (was {})",nextPreparedRoute); |
|
nextPreparedRoute = null; |
|
return null; |
|
} else { |
|
LOG.debug("was not able to start {}", nextPreparedRoute); |
|
} |
|
} |
|
if (isSet(routePrepper)) return t("Already searching route for {}",this); |
|
routePrepper = new RoutePrepper(new Context(this).block(currentBlock).direction(direction)); |
|
|
|
routePrepper.onRoutePrepared(() -> { |
|
Route newRoute = routePrepper.route(); |
|
LOG.debug("prepared route {} for {}",newRoute,this); |
|
newRoute.start(); |
|
routePrepper = null; |
|
plan.stream(t("Started {}",Train.this)); |
|
}); |
|
|
|
routePrepper.onFail(() -> { |
|
LOG.debug("preparing route for {} failed, resetting.",this); |
|
Route failedRoute = routePrepper.route(); |
|
routePrepper = null; |
|
if (isSet(failedRoute)) failedRoute.reset(); |
|
LOG.debug("Starting {} failed due to unavailable route!",this); |
|
plan.onChange(()->{ // wait for state change of plan |
|
if (autopilot) Train.this.start(false); |
|
}); |
|
}); |
|
|
|
routePrepper.start(); |
|
|
|
return null; |
|
} |
|
|
|
public static void startAll() { |
|
LOG.debug("Train.startAll()"); |
|
for (Train train : BaseClass.listElements(Train.class)) LOG.info(train.start(true)); |
|
} |
|
|
|
public void startBrake() { |
|
LOG.debug("{}.startBrake()",this); |
|
if (autopilot && isSet(nextPreparedRoute)) { |
|
LOG.debug("not braking, because autopilot is active and next roue is prepared, already"); |
|
return; |
|
} |
|
brake = new BrakeProcess(this); |
|
} |
|
|
|
public Window stopNow() { |
|
endBrake(); |
|
setSpeed(0); |
|
quitAutopilot(); |
|
if (isSet(route)) { |
|
if (route.hasTriggeredContacts()) { |
|
stuckTrace = new HashSet<Tile>(); |
|
for (Tile tile : route.path()) { // collect occupied tiles of route. stuckTrace is considered during next route search |
|
if (trace.contains(tile)) stuckTrace.add(tile); |
|
} |
|
} |
|
route.reset(); |
|
route = null; |
|
} |
|
return properties(); |
|
} |
|
|
|
public HashSet<Tile> stuckTrace() { |
|
return stuckTrace; |
|
} |
|
|
|
|
|
public SortedSet<String> tags() { |
|
TreeSet<String> list = new TreeSet<String>(tags); |
|
for (Car car:cars) list.addAll(car.tags()); |
|
return list; |
|
} |
|
|
|
public String toggleFunction(String name) { |
|
if (isNull(name)) return t("No function name passed to toggleFunction(…)"); |
|
return setFunction(name,!functionEnabled(name)); |
|
} |
|
|
|
public String setFunction(String name,boolean enable) { |
|
if (isNull(name)) return t("No function name passed to toggleFunction(…)"); |
|
LOG.debug("Setting function \"{}\" to {}",name,enable); |
|
locos().forEach(loco -> { |
|
Function any = null; |
|
for (Function f : loco.functions(name)) any = f.setState(enable); |
|
if (isSet(any)) loco.decoder().queue(); |
|
}); |
|
return null; |
|
} |
|
|
|
private Object toggleFunction(Params params) { |
|
return properties(toggleFunction(params.getString(FUNCTION))); |
|
} |
|
|
|
@Override |
|
public String toString() { |
|
return name(); |
|
} |
|
|
|
/** |
|
* this inverts the direction the train is heading to. Example: |
|
* before: CabCar→ MiddleCar→ Loco→ |
|
* after: ←CabCar ←MiddleCar ←Loco |
|
* @return |
|
*/ |
|
public Train turn() { |
|
LOG.debug("{}.turn()",this); |
|
reverse(cars); |
|
updateEnds(); |
|
for (Car car : cars) car.turn(); |
|
return reverse(); |
|
} |
|
|
|
private void updateEnds() { |
|
for (Car car : cars) car.isFirst(false).isLast(false); |
|
cars.firstElement().isFirst(true); |
|
cars.lastElement().isLast(true); |
|
} |
|
|
|
public void unTrace(Tile tile) { |
|
if (isSet(trace)) trace.remove(tile); |
|
if (isSet(stuckTrace)) stuckTrace.remove(tile); |
|
|
|
} |
|
|
|
protected Window update(Params params) { |
|
LOG.debug("update({})",params); |
|
pushPull = params.containsKey(PUSH_PULL) && "on".equals(params.get(PUSH_PULL)); |
|
shunting = params.containsKey(SHUNTING) && "on".equals(params.get(SHUNTING)); |
|
if (params.containsKey(NAME)) { |
|
name = params.getString(NAME); |
|
if (isSet(currentBlock)) plan.place(currentBlock); |
|
} |
|
if (params.containsKey(TAGS)) { |
|
String[] parts = params.getString(TAGS).replace(",", " ").split(" "); |
|
tags.clear(); |
|
for (String tag : parts) { |
|
tag = tag.trim(); |
|
if (!tag.isEmpty()) tags.add(tag); |
|
} |
|
} |
|
return properties(); |
|
} |
|
|
|
public Context updateTrace(Context context) { |
|
LOG.debug("updateTrace({})",context); |
|
Tile from = context.tile(); |
|
if (isNull(from)) from = context.contact(); |
|
if (isNull(from)) { |
|
LOG.debug("no starting point for trace given in {}",context); |
|
return context; |
|
} |
|
trace.add(from); |
|
Route route = context.route(); |
|
LOG.debug("Route: {}",route); |
|
if (isNull(route)) return context; |
|
Vector<Tile> reversedPath = reverse(route.path()); |
|
HashSet<Tile> newTrace = new HashSet<Tile>(); |
|
Integer remainingLength = null; |
|
|
|
for (Tile tile : reversedPath) { |
|
if (isNull(remainingLength) && onTrace(tile)) remainingLength = length(); |
|
if (remainingLength == null) { // ahead of train |
|
LOG.debug("{} is ahead of train and will not be touched.",tile); |
|
trace.remove(tile); // old trace will be cleared afterwards. but this tile shall not be cleared, so remove it from old trace |
|
} else if (remainingLength > 0) { // within train |
|
LOG.debug("{} is occupied by train and will be marked as \"occupied\"",tile); |
|
remainingLength -= tile.length(); |
|
newTrace.add(tile); |
|
trace.remove(tile); // old trace will be cleared afterwards. but this tile shall not be cleared, so remove it from old trace |
|
tile.setTrain(this); |
|
LOG.debug("remaining length: {}",remainingLength); |
|
} else { // behind train |
|
if (Route.freeBehindTrain) { |
|
LOG.debug("{} is behind train and will be freed in the next step",tile); |
|
trace.add(tile); // old trace will be cleared afterwards |
|
} else { |
|
LOG.debug("{} is behind train and will be reset to \"locked\" state",tile); |
|
tile.lockFor(context,true); |
|
trace.remove(tile); // old trace will be cleared afterwards. but this tile shall not be cleared, so remove it from old trace |
|
} |
|
} |
|
} |
|
dropTrace(false); |
|
trace = newTrace; |
|
return context; |
|
} |
|
|
|
|
|
|
|
public boolean usesAutopilot() { |
|
return autopilot; |
|
} |
|
} |
|
|
|
|
|
|