- added else branch to conditional actions

- implemented history for any object extending BaseClass (i.e. almost any)
- added distance() calculation method to base class
- Button "edit JSON" in Action Lists now hidden by default, can be enabled in plan properties

- fixed bugs:
    - updating json of DisplayText was not working properly
    - log output in Tile.isFreeFor was broken
This commit is contained in:
Stephan Richter
2021-04-09 18:55:20 +02:00
parent 559e095e33
commit 77166acf91
18 changed files with 252 additions and 38 deletions

View File

@@ -125,6 +125,8 @@ public class Application extends BaseClass{
return Condition.action(params,plan);
case REALM_CU:
return plan.controlUnit().process(params);
case REALM_HISTORY:
return History.action(params);
case REALM_LOCO:
return Locomotive.action(params,plan);
case REALM_MAINTENANCE:

View File

@@ -18,8 +18,9 @@ import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.srsoftware.tools.translations.Translation;
import de.srsoftware.tools.Tag;
import de.srsoftware.tools.translations.Translation;
import de.srsoftware.web4rail.History.LogEntry;
import de.srsoftware.web4rail.Plan.Direction;
import de.srsoftware.web4rail.actions.Action;
import de.srsoftware.web4rail.conditions.Condition;
@@ -54,7 +55,7 @@ public abstract class BaseClass implements Constants{
public static final Logger LOG = LoggerFactory.getLogger(BaseClass.class);
private static final String CUSTOM_FIELDS = "custom_Fields";
private static final String NEW_CUSTOM_FIELD_NAME = "new_custom_field_name";
protected HashMap<String,String> customFieldValues = new HashMap<String, String>();
protected HashMap<String,String> customFieldValues = new HashMap<String, String>();
private BaseClass parent;
public static class Context {
@@ -308,6 +309,12 @@ public abstract class BaseClass implements Constants{
}
}
public Window addLogEntry(String text) {
History.assign(new History.LogEntry(text),this);
return properties();
}
public Button button(String text,Map<String,String> additionalProps) {
return new Button(text,props(additionalProps));
}
@@ -320,6 +327,25 @@ public abstract class BaseClass implements Constants{
LOG.debug(tx, fills);
return false;
}
public static String distance(long l) {
String unit = Plan.lengthUnit;
if (DEFAULT_LENGTH_UNIT.equals(unit)) {
if (l > 1_000_000) {
l/=1_000_000;
unit = t("km");
} else
if (l > 1_000) {
l/=1_000;
unit = t("m");
} else
if (l > 10) {
l/=10;
unit = t("cm");
}
}
return l+NBSP+unit;
}
public Form form(String id,List<Map.Entry<String, Tag>> elements) {
Form form = new Form(id);
@@ -503,6 +529,21 @@ public abstract class BaseClass implements Constants{
new Button(t("Apply"),customForm).addTo(customForm).addTo(customFields);
customFields.addTo(win);
Fieldset history = new Fieldset(t("History"));
Form form = new Form("add-history-entry");
new Input(REALM, REALM_HISTORY).hideIn(form);
new Input(ACTION, ACTION_ADD).hideIn(form);
new Input(ID,id()).hideIn(form);
new TextArea(NOTES).addTo(form);
new Button(t("Add entry"), form).addTo(form);
form.addTo(history);
table = new Table();
table.addHead(t("Date/Time"),t("Event"));
for (LogEntry entry : History.getFor(this)) table.addRow(new SimpleDateFormat("YYYY-dd-MM HH:mm").format(entry.date()),entry.getText());
table.addTo(history).addTo(win);
return win;
}

View File

@@ -46,6 +46,7 @@ public interface Constants {
public static final String REALM_CONDITION = "condition";
public static final String REALM_CONTACT = "contact";
public static final String REALM_CU = "cu";
public static final String REALM_HISTORY = "history";
public static final String REALM_LOCO = "loco";
public static final String REALM_MAINTENANCE = "maintenance";
public static final String REALM_ROUTE = "route";

View File

@@ -0,0 +1,102 @@
package de.srsoftware.web4rail;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Vector;
import org.json.JSONObject;
import de.srsoftware.web4rail.BaseClass.Id;
public class History {
private static HashMap<BaseClass.Id, Vector<LogEntry>> log = new HashMap<>();
static class LogEntry extends BaseClass {
private long timestamp;
private String text;
public LogEntry(String text) {
this.text = text;
timestamp = new Date().getTime();
}
public Date date() {
return new Date(timestamp);
}
public long getTime() {
return timestamp;
}
public String getText() {
return text;
}
};
public static LogEntry assign(LogEntry logEntry, BaseClass object) {
Id id = object.id();
Vector<LogEntry> list = log.get(id);
if (list == null) log.put(id, list = new Vector<>());
list.insertElementAt(logEntry,0);
return logEntry;
}
public static Vector<LogEntry> getFor(BaseClass object){
Vector<LogEntry> list = log.get(object.id());
return list != null ? list : new Vector<>();
}
public static Object action(HashMap<String, String> params) {
switch (params.get(Constants.ACTION)) {
case Constants.ACTION_ADD:
BaseClass object = BaseClass.get(Id.from(params));
return object != null ? object.addLogEntry(params.get(Constants.NOTES)) : BaseClass.t("Unknown object!");
}
return BaseClass.t("Unknown action: {}",params.get(Constants.ACTION));
}
public static void save(String filename) {
try {
FileWriter file = new FileWriter(filename, Constants.UTF8);
JSONObject json = new JSONObject();
log.entrySet().forEach(entry -> {
JSONObject list = new JSONObject();
entry.getValue().forEach(le -> list.put(le.timestamp+"", le.getText()));
json.put(entry.getKey().toString(), list);
});
json.write(file);
file.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void load(String filename) throws IOException {
BufferedReader file = new BufferedReader(new FileReader(filename, Constants.UTF8));
JSONObject json = new JSONObject(file.readLine());
file.close();
for (String id : json.keySet()) {
JSONObject o = json.getJSONObject(id);
Vector<LogEntry> entries = new Vector<>();
for (String time : o.keySet()) {
LogEntry le = new LogEntry(o.getString(time));
le.timestamp = Long.parseLong(time);
entries.add(le);
}
Collections.sort(entries, (a,b) -> Long.compare(b.timestamp, a.timestamp));
log.put(new Id(id), entries);
}
}
}

View File

@@ -84,6 +84,7 @@ public class MaintnanceTask extends BaseClass{
Block block = train.currentBlock();
if (isSet(block)) plan.place(block);
}
car.addLogEntry(t("executed \"{}\" after {}",name,BaseClass.distance(lastExecutionDist)));
return car.properties();
}
return t("parent is not a car!");

View File

@@ -157,12 +157,14 @@ public class Plan extends BaseClass{
private static final String FREE_BEHIND_TRAIN = "free_behind_train";
private static final String RENAME = "rename";
private static final String SPEED_STEP = "speed_step";
private static final String ALLOW_JSON_EDIT = "allow_json_edit";
private String name = DEFAULT_NAME;
private ControlUnit controlUnit = new ControlUnit(this); // the control unit, to which the plan is connected
private Contact learningContact;
private Configuration appConfig;
private LinkedList<EventListener> listeners = new LinkedList<>();
public static boolean allowJsonEdit = false;
/**
* creates a new plan, starts to send heart beats
@@ -563,6 +565,9 @@ public class Plan extends BaseClass{
} catch (Exception e) {
LOG.warn("Was not able to establish connection to control unit!");
}
History.load(name+".history");
LoadCallback.fire();
}
@@ -714,6 +719,7 @@ public class Plan extends BaseClass{
formInputs.add(t("Speed step"),new Input(SPEED_STEP, Train.defaultSpeedStep).attr("title", t("Speeds are always increadsed/decreased by this value")));
formInputs.add(t("Lower speed limit"),new Input(FINAL_SPEED, Train.defaultEndSpeed).attr("title", t("Final speed after breaking, before halting")));
formInputs.add(t("Free tiles behind train"),new Checkbox(FREE_BEHIND_TRAIN, t("If checked, tiles behind the train are freed according to the length of the train and the tiles. If it is unchecked, tiles will not get free before route is finished."), Route.freeBehindTrain));
formInputs.add(t("Allow editing JSON of action lists"),new Checkbox(ALLOW_JSON_EDIT, t("Do you know, what you are doing?"), allowJsonEdit ));
postForm.add(relayProperties());
postForm.add(routeProperties());
@@ -859,6 +865,8 @@ public class Plan extends BaseClass{
file.write(json().toString());
file.close();
History.save(name+".history");
return t("Plan saved as \"{}\".",name);
}
@@ -1026,7 +1034,8 @@ public class Plan extends BaseClass{
if (params.containsKey(LENGTH_UNIT)) lengthUnit = params.get(LENGTH_UNIT);
if (params.containsKey(SPEED_UNIT)) speedUnit = params.get(SPEED_UNIT);
if (params.containsKey(SPEED_STEP)) Train.defaultSpeedStep = Integer.parseInt(params.get(SPEED_STEP));
if (params.containsKey(FINAL_SPEED)) Train.defaultEndSpeed = Integer.parseInt(params.get(FINAL_SPEED));
if (params.containsKey(FINAL_SPEED)) Train.defaultEndSpeed = Integer.parseInt(params.get(FINAL_SPEED));
allowJsonEdit = "on".equalsIgnoreCase(params.get(ALLOW_JSON_EDIT));
Route.freeBehindTrain = "on".equalsIgnoreCase(params.get(FREE_BEHIND_TRAIN));
return t("Plan updated.");

View File

@@ -348,7 +348,7 @@ public class Route extends BaseClass {
setupActions = new ActionList(this);
triggeredActions.put(ROUTE_SETUP, setupActions);
}
setupActions.list().addTo(setup).addTo(list);
setupActions.listAt(setup).addTo(list);
Tag start = new Tag("li").content(t("Start actions")+COL);
ActionList startActions = triggeredActions.get(ROUTE_START);
@@ -356,7 +356,7 @@ public class Route extends BaseClass {
startActions = new ActionList(this);
triggeredActions.put(ROUTE_START, startActions);
}
startActions.list().addTo(start).addTo(list);
startActions.listAt(start).addTo(list);
for (Contact c : contacts) {
Tag item = c.link("span", c).addTo(new Tag("li")).content(NBSP);
@@ -365,7 +365,7 @@ public class Route extends BaseClass {
actions = new ActionList(this);
triggeredActions.put(c.trigger(), actions);
}
actions.list().addTo(item).addTo(list);
actions.listAt(item).addTo(list);
}
list.addTo(win);
return win;

View File

@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
import de.srsoftware.tools.Tag;
import de.srsoftware.web4rail.BaseClass;
import de.srsoftware.web4rail.LoadCallback;
import de.srsoftware.web4rail.tags.Button;
import de.srsoftware.web4rail.tags.Fieldset;
import de.srsoftware.web4rail.tags.Form;
@@ -110,6 +111,7 @@ public abstract class Action extends BaseClass {
((ActionList)this).clear();
}
load(json);
LoadCallback.fire();
return context().properties();
}
Window win = new Window("json-import-export-"+id(), t("JSON code of {}",this));

View File

@@ -123,23 +123,22 @@ public class ActionList extends Action implements Iterable<Action>{
return json;
}
public Tag list() {
Tag span = new Tag("span");
button(t("add action"), Map.of(ACTION, ACTION_ADD)).addTo(span);
button(t("export"), Map.of(ACTION, ACTION_SAVE)).addTo(span);
public <T extends Tag> T listAt(T parent) {
button(parent.is("fieldset") ? t("add action") : "+", Map.of(ACTION, ACTION_ADD)).title(t("add action")).addTo(parent);
if (plan.allowJsonEdit) button(t("edit JSON"), Map.of(ACTION, ACTION_SAVE)).addTo(parent);
if (!isEmpty()) {
Tag list = new Tag("ol");
for (Action action : actions) {
Tag item = action.link("span",action).addTo(new Tag("li")).content(NBSP);
action.button("-", Map.of(ACTION,ACTION_DROP)).addTo(item);
action.button("", Map.of(ACTION,ACTION_MOVE)).addTo(item);
if (action instanceof ActionList) ((ActionList) action).list().addTo(item);
action.button("", Map.of(ACTION,ACTION_MOVE)).title(t("move up")).addTo(item);
action.button("-", Map.of(ACTION,ACTION_DROP)).title(t("delete")).addTo(item);
if (action instanceof ActionList) ((ActionList) action).listAt(item);
item.addTo(list);
}
list.addTo(span);
list.addTo(parent);
}
return span;
return (T)parent;
}
public Action load(JSONObject json) {
@@ -248,9 +247,7 @@ public class ActionList extends Action implements Iterable<Action>{
@Override
protected Window properties(List<Fieldset> preForm, FormInput formInputs, List<Fieldset> postForm,String...errors) {
Fieldset fieldset = new Fieldset(t("Actions"));
list().addTo(fieldset);
postForm.add(fieldset);
preForm.add(listAt(new Fieldset(t("Actions")).clazz("actions")));
return super.properties(preForm, formInputs, postForm,errors);
}

View File

@@ -2,10 +2,13 @@ package de.srsoftware.web4rail.actions;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.Vector;
import org.json.JSONArray;
import org.json.JSONObject;
import de.srsoftware.tools.Tag;
import de.srsoftware.web4rail.BaseClass;
import de.srsoftware.web4rail.conditions.Condition;
import de.srsoftware.web4rail.conditions.ConditionList;
@@ -15,11 +18,14 @@ import de.srsoftware.web4rail.tags.Window;
public class ConditionalAction extends ActionList {
private static final String CONDITIONS = "conditions";
private static final String ELSE_ACTONS = "else_actions";
private ConditionList conditions = new ConditionList();
private ActionList elseActions;
public ConditionalAction(BaseClass parent) {
super(parent);
conditions.parent(this);
elseActions = new ActionList(parent);
}
public boolean equals(ConditionalAction other) {
@@ -29,7 +35,7 @@ public class ConditionalAction extends ActionList {
@Override
public boolean fire(Context context,Object cause) {
for (Condition condition : conditions) {
if (!condition.fulfilledBy(context)) return true; // wenn die Bedingung nicht erfüllt ist, ist das kein Fehler!
if (!condition.fulfilledBy(context)) return elseActions.fire(context, cause);
}
return super.fire(context.clone(),cause); // actions, that happen within the conditional action list must not modify the global context.
}
@@ -48,13 +54,27 @@ public class ConditionalAction extends ActionList {
JSONArray conditions = new JSONArray();
for (Condition condition : this.conditions) conditions.put(condition.json());
json.put(CONDITIONS, conditions);
if (!elseActions.isEmpty()) json.put(ELSE_ACTONS, elseActions.json());
return json;
}
@Override
public <T extends Tag> T listAt(T parent) {
T tag = super.listAt(parent);
if (!elseActions.isEmpty()) {
Tag div = new Tag("div").clazz("else");
new Tag("span").content(t("else:")+NBSP).addTo(div);
elseActions.listAt(div);
div.addTo(tag);
}
return tag;
}
@Override
public Action load(JSONObject json) {
super.load(json);
super.load(json);
if (json.has(CONDITIONS)) {
conditions.clear();
for (Object o : json.getJSONArray(CONDITIONS)) {
if (o instanceof JSONObject) {
JSONObject j = (JSONObject) o;
@@ -66,13 +86,37 @@ public class ConditionalAction extends ActionList {
}
}
}
if (json.has(ELSE_ACTONS)) elseActions.load(json.getJSONObject(ELSE_ACTONS));
return this;
}
@Override
protected Window properties(List<Fieldset> preForm, FormInput formInputs, List<Fieldset> postForm,String...errors) {
preForm.add(conditions.list());
return super.properties(preForm, formInputs, postForm,errors);
Window win = super.properties(preForm, formInputs, postForm,errors);
Optional<Fieldset> actionFieldSet = win.children()
.stream()
.filter(tag -> tag instanceof Fieldset)
.map(tag -> (Fieldset)tag)
.filter(fs -> "actions".equals(fs.get("class")))
.findFirst();
if (actionFieldSet.isPresent()) {
Vector<Tag> children = actionFieldSet.get().children();
children.insertElementAt(new Tag("h3").content(t("Actions in case conditions are fulfilled")),1);
LOG.debug("children: "+children);
Optional<Tag> elseTag = children.stream().filter(tag -> "else".equals(tag.get("class"))).findFirst();
if (elseTag.isPresent()) {
children = elseTag.get().children();
children.remove(0);
children.insertElementAt(new Tag("h3").content(t("Actions in case conditions are <em>not</em> fulfilled")),0);
} else {
children.add(new Tag("h3").content(t("Actions in case conditions are <em>not</em> fulfilled")));
elseActions.listAt(actionFieldSet.get());
}
}
return win;
}

View File

@@ -69,14 +69,14 @@ public class WaitForContact extends ActionList {
}
@Override
public Tag list() {
Tag list = super.list();
public <T extends Tag> T listAt(T parent) {
T list = super.listAt(parent);
for (Tag child : list.children()) {
if (child.is("ol")) {
break;
}
}
timeoutActions.list().addTo(new Tag("span").content(t("On timeout (after {} ms)",timeout)+":")).addTo(list);
timeoutActions.listAt(new Tag("span").content(t("On timeout (after {} ms)",timeout)+":")).addTo(list);
return list;
}
@@ -103,7 +103,7 @@ public class WaitForContact extends ActionList {
Fieldset fieldset = new Fieldset(t("Actions on timeout"));
fieldset.id("actions");
timeoutActions.list().addTo(fieldset);
timeoutActions.listAt(fieldset);
postForm.add(fieldset);
return super.properties(preForm, formInputs, postForm,errors);

View File

@@ -31,6 +31,9 @@ public class ConditionList extends Condition implements Iterable<Condition>{
this.conditions.addAll(conditions.conditions);
}
public void clear() {
conditions.clear();
}
public boolean fulfilledBy(Context context) {
for (Condition condition : conditions) {

View File

@@ -189,7 +189,7 @@ public class Contact extends Tile{
formInputs.add(t("Address"),span);
Fieldset fieldset = new Fieldset(t("Actions")).id("props-actions");
actions.list().addTo(fieldset);
actions.listAt(fieldset);
postForm.add(fieldset);
return super.properties(preForm, formInputs, postForm,errors);
}

View File

@@ -129,11 +129,11 @@ public class Switch extends Tile{
protected Window properties(List<Fieldset> preForm, FormInput formInputs, List<Fieldset> postForm,String...errors) {
Fieldset fieldset = new Fieldset(t("Actions (On)"));
fieldset.id("actionsOn");
actionsOn.list().addTo(fieldset);
actionsOn.listAt(fieldset);
postForm.add(fieldset);
fieldset = new Fieldset(t("Actions (Off)"));
fieldset.id("actionsOff");
actionsOff.list().addTo(fieldset);
actionsOff.listAt(fieldset);
postForm.add(fieldset);
return super.properties(preForm, formInputs, postForm,errors);
}

View File

@@ -149,18 +149,18 @@ public abstract class Tile extends BaseClass implements Comparable<Tile> {
Train train = newTrain.train();
if (isSet(reservingTrain) && reservingTrain != train) {
LOG.debug("{} is reserved for {}",reservingTrain);
LOG.debug("{} is reserved for {}",this,reservingTrain);
return false; // nicht in reservierten Block einfahren!
}
if (isSet(lockingTrain) && lockingTrain != train) {
LOG.debug("{} is locked for {}",lockingTrain);
LOG.debug("{} is locked for {}",this,lockingTrain);
return false; // nicht in reservierten Block einfahren!
}
if (isSet(occupyingTrain) && occupyingTrain != train) {
LOG.debug("{} is occupied by {}",occupyingTrain);
return train.isShunting(); // nur in belegte Blöcke einfahren, wenn Rangiermodus aktiv!
LOG.debug("{} is occupied by {}",this,occupyingTrain);
return isSet(train) && train.isShunting(); // nur in belegte Blöcke einfahren, wenn Rangiermodus aktiv!
}
return true;