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.
 
 
 
 

704 lines
21 KiB

package de.srsoftware.web4rail;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Vector;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.keawe.tools.translations.Translation;
import de.srsoftware.tools.Tag;
import de.srsoftware.web4rail.Plan.Direction;
import de.srsoftware.web4rail.actions.Action;
import de.srsoftware.web4rail.actions.Action.Context;
import de.srsoftware.web4rail.actions.ActionList;
import de.srsoftware.web4rail.actions.FinishRoute;
import de.srsoftware.web4rail.actions.SetSignalsToStop;
import de.srsoftware.web4rail.actions.SetSpeed;
import de.srsoftware.web4rail.conditions.Condition;
import de.srsoftware.web4rail.moving.Train;
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.tiles.Block;
import de.srsoftware.web4rail.tiles.Contact;
import de.srsoftware.web4rail.tiles.Shadow;
import de.srsoftware.web4rail.tiles.Signal;
import de.srsoftware.web4rail.tiles.Tile;
import de.srsoftware.web4rail.tiles.Turnout;
import de.srsoftware.web4rail.tiles.Turnout.State;
/**
* A route is a vector of tiles that leads from one block to another.
*
* @author Stephan Richter, SRSoftware
*
*/
public class Route extends BaseClass{
private static final Logger LOG = LoggerFactory.getLogger(Route.class);
private static final String ACTION_LISTS = "action_lists";
private static final String ACTIONS = "actions";
private static final String CONDITIONS = "conditions";
private static final String DROP_CONDITION = "drop_condition";
private static final String END_DIRECTION = "direction_end";
private static final String ROUTES = "routes";
private static final String START_DIRECTION = "direction_start";
private static final String TRIGGER = "trigger";
static final String NAME = "name";
static final String PATH = "path";
static final String SIGNALS = "signals";
static final String TURNOUTS = "turnouts";
private static HashMap<Integer, String> names = new HashMap<Integer, String>(); // maps id to name. needed to keep names during plan.analyze()
private Vector<Condition> conditions = new Vector<Condition>();
private Vector<Contact> contacts;
private boolean disabled = false;
private Block endBlock = null;
public Direction endDirection;
private int id;
private Vector<Tile> path;
private Vector<Signal> signals;
public Train train;
private HashMap<String,ActionList> triggers = new HashMap<String, ActionList>();
private HashMap<Turnout,Turnout.State> turnouts;
private ActionList setupActions = new ActionList();
private Block startBlock = null;
public Direction startDirection;
private HashSet<Contact> triggeredContacts = new HashSet<>();
/**
* process commands from the client
* @param params
* @return
* @throws IOException
*/
public static Object action(HashMap<String, String> params) throws IOException {
Route route = plan.route(Integer.parseInt(params.get(ID)));
if (isNull(route)) return t("Unknown route: {}",params.get(ID));
switch (params.get(ACTION)) {
case ACTION_DROP:
String message = plan.remove(route);
String tileId = params.get(Tile.class.getSimpleName());
if (isSet(tileId)) {
Tile tile = plan.get(tileId, false);
if (isSet(tile)) {
plan.stream(message);
return tile.propMenu();
}
}
return message;
case ACTION_PROPS:
return route.properties(params);
case ACTION_UPDATE:
return route.update(params,plan);
case DROP_CONDITION:
return route.dropCodition(params);
}
return t("Unknown action: {}",params.get(ACTION));
}
/**
* adds a tile to the route
* @param tile
* @param direrction
* @return
*/
public Tile add(Tile tile, Direction direrction) {
if (tile instanceof Shadow) tile = ((Shadow)tile).overlay();
if (tile instanceof Block) {
endBlock = (Block) tile;
endDirection = direrction;
}
path.add(tile);
if (tile instanceof Contact) contacts.add((Contact) tile);
if (tile instanceof Signal) {
Signal signal = (Signal) tile;
if (signal.isAffectedFrom(direrction)) addSignal(signal);
}
return tile;
}
/**
* adds a action to the action list of the given trigger
* @param trigger
* @param action
*/
public void add(String trigger, Action action) {
ActionList actions = triggers.get(trigger);
if (isNull(actions)) {
actions = new ActionList();
triggers.put(trigger, actions);
}
actions.add(action);
}
private void addBasicPropertiesTo(Window win) {
if (isSet(train)) link("span",Map.of(REALM,REALM_TRAIN,ID,train.id,ACTION,ACTION_PROPS),t("Train: {}",train)).addTo(win);
new Tag("h4").content(t("Origin and destination")).addTo(win);
Tag list = new Tag("ul");
Plan.addLink(startBlock, t("Origin: {} to {}",startBlock.name,startDirection), list);
Plan.addLink(endBlock, t("Destination: {} from {}",endBlock.name,endDirection), list);
list.addTo(win);
if (!signals.isEmpty()) {
new Tag("h4").content(t("Signals")).addTo(win);
list = new Tag("ul");
for (Signal s : signals) Plan.addLink(s,s.toString(),list);
list.addTo(win);
}
}
private void addConditionsTo(Window win) {
new Tag("h4").content(t("Conditions")).addTo(win);
if (!conditions.isEmpty()) {
Tag list = new Tag("ul");
for (Condition condition : conditions) {
Tag li = new Tag("li");
link("span",Map.of(REALM,REALM_CONDITION,ID,condition.id(),ACTION,ACTION_PROPS,CONTEXT,REALM_ROUTE+":"+id),condition).addTo(li);
Map<String, Object> params = Map.of(REALM,REALM_ROUTE,ID,id(),ACTION,DROP_CONDITION,REALM_CONDITION,condition.id());
new Button(t("delete"), params).addTo(li.content(NBSP)).addTo(list);
}
list.addTo(win);
}
new Tag("div").content(t("Route will only be available, if all conditions are fulfilled.")).addTo(win);
Form form = new Form("action-prop-form-"+id);
Fieldset fieldset = new Fieldset(t("Add condition"));
new Input(REALM,REALM_ROUTE).hideIn(form);
new Input(ID,id()).hideIn(form);
new Input(ACTION,ACTION_UPDATE).hideIn(form);
Condition.selector().addTo(fieldset);
new Button(t("Add condition"),form).addTo(fieldset).addTo(form).addTo(win);
}
private void addContactsTo(Window win) {
if (!contacts.isEmpty()) {
new Tag("h4").content(t("Actions and contacts")).addTo(win);
Tag list = new Tag("ol");
Tag setup = new Tag("li").content(t("Setup actions"));
setupActions.addTo(setup, context());
setup.addTo(list);
for (Contact c : contacts) {
Tag link = Plan.addLink(c,c.toString(),list);
ActionList actions = triggers.get(c.trigger());
if (isNull(actions)) {
actions = new ActionList();
triggers.put(c.trigger(), actions);
}
actions.addTo(link,context());
}
list.addTo(win);
}
}
private void addFormTo(Window win, HashMap<String, String> params) {
Form form = new Form("route-"+id+"-props");
new Input(ACTION, ACTION_UPDATE).hideIn(form);
new Input(REALM,REALM_ROUTE).hideIn(form);
new Input(ID,id()).hideIn(form);
if (params.containsKey(CONTEXT)) new Input(CONTEXT,params.get(CONTEXT)).hideIn(form);
new Input(NAME, name()).style("width: 80%").addTo(new Label(t("name:")+NBSP)).addTo(form);
new Checkbox(DISABLED, t("disabled"), disabled).addTo(form);
new Button(t("Apply"),form).addTo(form).addTo(win);
}
public void addPropertiesFrom(Route existingRoute) {
LOG.debug("addPropertiesFrom({})",existingRoute);
disabled = existingRoute.disabled;
setupActions.addActionsFrom(existingRoute.setupActions);
for (Entry<String, ActionList> entry : triggers.entrySet()) {
String trigger = entry.getKey();
ActionList existingActionList = existingRoute.triggers.get(trigger);
if (isSet(existingActionList)) {
LOG.debug("found action list for {} on existing route {}: {}",trigger,existingRoute,existingActionList);
ActionList newActionList = entry.getValue();
newActionList.addActionsFrom(existingActionList);
}
}
}
void addSignal(Signal signal) {
signals.add(signal);
}
void addTurnout(Turnout t, State s) {
turnouts.put(t, s);
}
private void addTurnoutsTo(Window win) {
if (!turnouts.isEmpty()) {
new Tag("h4").content(t("Turnouts")).addTo(win);
Tag list = new Tag("ul");
for (Entry<Turnout, State> entry : turnouts.entrySet()) {
Turnout turnout = entry.getKey();
Plan.addLink(turnout, turnout+": "+t(entry.getValue().toString()), list);
}
list.addTo(win);
}
}
/**
* checks, whether the route may be used in a given context
* @param context
* @return false, if any of the associated conditions is not fulfilled
*/
public boolean allowed(Context context) {
if (disabled) return false;
for (Condition condition : conditions) {
if (!condition.fulfilledBy(context)) return false;
}
return true;
}
public Route begin(Block block,Direction from) {
// add those fields to clone, too!
contacts = new Vector<Contact>();
signals = new Vector<Signal>();
path = new Vector<Tile>();
turnouts = new HashMap<>();
startBlock = block;
startDirection = from;
path.add(block);
return this;
}
protected Route clone() {
Route clone = new Route();
clone.startBlock = startBlock;
clone.startDirection = startDirection;
clone.endBlock = endBlock;
clone.endDirection = endDirection;
clone.contacts = new Vector<Contact>(contacts);
clone.signals = new Vector<Signal>(signals);
clone.turnouts = new HashMap<>(turnouts);
clone.path = new Vector<>(path);
return clone;
}
public void complete() {
if (contacts.size()>1) { // mindestens 2 Kontakte: erster Kontakt aktiviert Block, vorletzter Kontakt leitet Bremsung ein
Contact nextToLastContact = contacts.get(contacts.size()-2);
add(nextToLastContact.trigger(),new SetSpeed().speed(30));
add(nextToLastContact.trigger(),new SetSignalsToStop());
}
if (!contacts.isEmpty()) {
Contact lastContact = contacts.lastElement();
add(lastContact.trigger(), new SetSpeed());
add(lastContact.trigger(), new FinishRoute());
}
}
/**
* Kontakt der Route aktivieren
* @param contact
* @param trainHead
*/
public void contact(Contact contact) {
if (triggeredContacts.contains(contact)) return; // don't trigger contact a second time
triggeredContacts.add(contact);
LOG.debug("{} on {} activated {}.",train,this,contact);
traceTrainFrom(contact);
ActionList actions = triggers.get(contact.trigger());
if (isNull(actions)) return;
Context context = new Context(contact);
actions.fire(context);
}
public Vector<Contact> contacts() {
return new Vector<>(contacts);
}
public String context() {
return REALM_ROUTE+":"+id();
}
public boolean isDisabled() {
return disabled;
}
private Object dropCodition(HashMap<String, String> params) {
String condId = params.get(REALM_CONDITION);
if (isSet(condId)) {
int cid = Integer.parseInt(condId);
for (Condition condition : conditions) {
if (condition.id() == cid) {
conditions.remove(condition);
break;
}
}
}
return properties(params);
}
public Block endBlock() {
return endBlock;
}
public void finish() {
setSignals(Signal.STOP);
for (Tile tile : path) tile.setRoute(null);
Tile lastTile = path.lastElement();
if (lastTile instanceof Contact) {
lastTile.set(null);
if (isSet(train)) train.removeFromTrace(lastTile);
}
if (isSet(train)) {
train.set(endBlock);
train.heading(endDirection.inverse());
if (endBlock == train.destination()) {
train.destination(null).quitAutopilot();
plan.stream(t("{} reached it`s destination!",train));
} else {
train.setWaitTime(endBlock.getWaitTime(train));
}
if (train.route == this) train.route = null;
}
train = null;
triggeredContacts.clear();
}
public boolean fireSetupActions(Context context) {
return setupActions.fire(context);
}
private String generateName() {
StringBuilder sb = new StringBuilder();
for (int i=0; i<path.size();i++) {
Tile tile = path.get(i);
if (i>0) sb.append("-");
if (tile instanceof Block) {
sb.append(((Block)tile).name);
if (i>0) break; // Kontakt nach dem Ziel-Block nicht mitnehmen
} else {
sb.append(tile.id());
}
}
return sb.toString();
}
public int id() {
if (id == 0) id = generateName().hashCode();
return id;
}
public boolean isFreeFor(Train newTrain) {
for (int i=1; i<path.size(); i++) {
if (!path.get(i).isFreeFor(newTrain)) return false;
}
return true;
}
/**
* creates a json representation of this route
* @return
*/
public String json() {
JSONObject json = new JSONObject();
json.put(ID, id());
Vector<String> tileIds = new Vector<String>();
for (Tile t : this.path) tileIds.add(t.id());
json.put(PATH, tileIds);
Vector<String> signalIds = new Vector<String>(); // list all signals affecting this route
for (Tile t : this.signals) signalIds.add(t.id());
json.put(SIGNALS, signalIds);
JSONArray turnouts = new JSONArray();
for (Entry<Turnout, State> entry : this.turnouts.entrySet()) {
Turnout t = entry.getKey();
turnouts.put(new JSONObject(Map.of(Turnout.ID,t.id(),Turnout.STATE,entry.getValue())));
}
json.put(TURNOUTS, turnouts);
json.put(START_DIRECTION, startDirection);
json.put(END_DIRECTION, endDirection);
JSONArray jConditions = new JSONArray();
for (Condition condition : conditions) jConditions.put(condition.json());
if (!jConditions.isEmpty()) json.put(CONDITIONS, jConditions);
JSONArray jTriggers = new JSONArray();
for (Entry<String, ActionList> entry : triggers.entrySet()) {
JSONObject trigger = new JSONObject();
trigger.put(TRIGGER, entry.getKey());
ActionList actionList = entry.getValue();
trigger.put(ACTIONS, actionList.json());
jTriggers.put(trigger);
}
if (!jTriggers.isEmpty()) json.put(ACTION_LISTS, jTriggers);
if (!setupActions.isEmpty()) json.put(ACTIONS, setupActions.json());
String name = name();
if (isSet(name)) json.put(NAME, name);
if (disabled) json.put(DISABLED, true);
return json.toString();
}
private Route load(JSONObject json,Plan plan) {
if (json.has(ID)) id = json.getInt(ID);
if (json.has(NAME)) name(json.getString(NAME));
JSONArray pathIds = json.getJSONArray(PATH);
startDirection = Direction.valueOf(json.getString(START_DIRECTION));
endDirection = Direction.valueOf(json.getString(END_DIRECTION));
for (Object tileId : pathIds) {
Tile tile = plan.get((String) tileId,false);
if (isNull(tile)) {
continue;
}
if (isNull(startBlock)) {
begin((Block) tile, startDirection);
} else if (tile instanceof Block) { // make sure, endDirection is set on last block
add(tile,endDirection);
} else {
add(tile, null);
}
}
if (isNull(path) || path.isEmpty()) {
LOG.warn("{} has no tiles. It will be ignored.",this);
return null;
}
if (json.has(TURNOUTS)) {
JSONArray turnouts = json.getJSONArray(TURNOUTS);
for (int i=0; i<turnouts.length();i++) {
JSONObject jTurnout = turnouts.getJSONObject(i);
Turnout turnout = (Turnout) plan.get(jTurnout.getString(Turnout.ID), false);
addTurnout(turnout, Turnout.State.valueOf(jTurnout.getString(Turnout.STATE)));
}
}
if (json.has(SIGNALS)) {
for (Object signalId : json.getJSONArray(SIGNALS)) addSignal((Signal) plan.get((String) signalId, false));
}
if (json.has(ACTION_LISTS)) loadActions(json.getJSONArray(ACTION_LISTS));
if (json.has(CONDITIONS)) loadConditions(json.getJSONArray(CONDITIONS));
if (json.has(ACTIONS)) {
setupActions = ActionList.load(json.getJSONArray(ACTIONS));
}
if (json.has(DISABLED)) disabled = json.getBoolean(DISABLED);
return plan.registerRoute(this);
}
private void loadActions(JSONArray arr) {
for (int i=0; i<arr.length(); i++) {
JSONObject json = arr.getJSONObject(i);
String trigger = json.getString(TRIGGER);
ActionList actionList = ActionList.load(json.getJSONArray(ACTIONS));
triggers.put(trigger, actionList);
}
}
public static void loadAll(String filename, Plan plan) throws IOException {
FileInputStream fis = new FileInputStream(filename);
JSONTokener tokener = new JSONTokener(fis);
JSONObject json = new JSONObject(tokener);
JSONArray routes = json.getJSONArray(ROUTES);
for (Object o : routes) {
if (o instanceof JSONObject) new Route().load((JSONObject)o, plan);
}
fis.close();
LOG.debug("json: {}",json.getClass());
}
private void loadConditions(JSONArray arr) {
for (int i=0; i<arr.length(); i++) {
JSONObject json = arr.getJSONObject(i);
Condition condition = Condition.create(json.getString(TYPE));
if (isSet(condition)) conditions.add(condition.load(json));
}
}
public boolean lock() {
Vector<Tile> alreadyLocked = new Vector<Tile>();
boolean success = true;
for (Tile tile : path) {
try {
tile.setRoute(this);
} catch (IllegalStateException e) {
success = false;
break;
}
}
if (!success) for (Tile tile :alreadyLocked) {
tile.setRoute(null);
}
return success;
}
public List<Route> multiply(int size) {
Vector<Route> routes = new Vector<Route>();
for (int i=0; i<size; i++) routes.add(i==0 ? this : this.clone());
return routes;
}
public String name() {
String name = names.get(id());
if (isNull(name)) {
name = generateName();
name(name);
}
return name;
}
public void name(String name) {
names.put(id(),name);
}
public Vector<Tile> path() {
Vector<Tile> result = new Vector<Tile>();
if (isSet(path)) result.addAll(path);
return result;
}
public Window properties(HashMap<String, String> params) {
Window win = new Window("route-properties",t("Properties of {}",this));
addFormTo(win,params);
addBasicPropertiesTo(win);
addTurnoutsTo(win);
addConditionsTo(win);
addContactsTo(win);
return win;
}
public boolean reset() {
setSignals(Signal.STOP);
for (Tile tile : path) tile.setRoute(null);
Tile lastTile = path.lastElement();
if (lastTile instanceof Contact) {
lastTile.set(null);
if (isSet(train)) train.removeFromTrace(lastTile);
}
if (isSet(train)) {
train.set(startBlock);
train.heading(startDirection);
if (train.route == this) train.route = null;
train = null;
}
triggeredContacts.clear();
return true;
}
public static void saveAll(Collection<Route> routes, String filename) throws IOException {
BufferedWriter file = new BufferedWriter(new FileWriter(filename));
file.write("{\""+ROUTES+"\":[\n");
int count = 0;
for (Route route : routes) {
file.write(route.json());
if (++count < routes.size()) file.write(",");
file.write("\n");
}
file.write("]}");
file.close();
}
public void setLast(State state) {
if (isNull(state) || state == State.UNDEF) return;
Tile lastTile = path.lastElement();
if (lastTile instanceof Turnout) addTurnout((Turnout) lastTile,state);
}
public boolean setSignals(String state) {
for (Signal signal : signals) {
if (!signal.state(isNull(state) ? Signal.GO : state)) return false;
}
return true;
}
public boolean setTurnouts() {
Turnout turnout = null;
for (Entry<Turnout, State> entry : turnouts.entrySet()) try {
turnout = entry.getKey();
State targetVal = entry.getValue();
if (!turnout.state(targetVal).succeeded()) return false;
try {
Thread.sleep(500);
} catch (InterruptedException e) {}
} catch (IOException e) {
LOG.warn("Was not able to switch turnout {}!",turnout,e);
return false;
}
return true;
}
public String shortName() {
String[] parts = name().split("-");
return parts[0].trim()+"–"+parts[parts.length-1].trim();
}
public Block startBlock() {
return startBlock;
}
protected static String t(String txt, Object...fills) {
return Translation.get(Application.class, txt, fills);
}
@Override
public String toString() {
return getClass().getSimpleName()+"("+(isSet(train)?train+":":"")+name()+")";
}
private void traceTrainFrom(Tile tile) {
Vector<Tile> trace = new Vector<Tile>();
for (Tile t:path) {
trace.add(t);
if (t == tile) break;
}
if (isSet(train)) train.addToTrace(trace);
}
public boolean train(Train newTrain) {
if (isSet(train) && newTrain != train) return false;
train = newTrain;
return true;
}
public Route unlock() throws IOException {
// TODO
return this;
}
public Object update(HashMap<String, String> params,Plan plan) {
LOG.debug("update({})",params);
String name = params.get(NAME);
if (isSet(name)) name(name);
disabled = "on".equals(params.get(DISABLED));
Condition condition = Condition.create(params.get(REALM_CONDITION));
if (isSet(condition)) {
conditions.add(condition);
return properties(params);
}
String message = t("{} updated.",this);
if (params.containsKey(CONTEXT)) {
plan.stream(message);
return plan.showContext(params);
}
return message;
}
}