package de.srsoftware.web4rail; import java.io.IOException; import java.io.OutputStreamWriter; import java.lang.reflect.InvocationTargetException; import java.security.InvalidParameterException; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.SortedSet; import java.util.Stack; import java.util.TreeSet; import java.util.Vector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.keawe.tools.translations.Translation; import de.srsoftware.tools.Tag; import de.srsoftware.web4rail.moving.Car; import de.srsoftware.web4rail.moving.Train; import de.srsoftware.web4rail.tags.Div; import de.srsoftware.web4rail.tiles.Block; import de.srsoftware.web4rail.tiles.BlockH; import de.srsoftware.web4rail.tiles.BlockV; import de.srsoftware.web4rail.tiles.Contact; import de.srsoftware.web4rail.tiles.ContactH; import de.srsoftware.web4rail.tiles.ContactV; import de.srsoftware.web4rail.tiles.CrossH; import de.srsoftware.web4rail.tiles.CrossV; import de.srsoftware.web4rail.tiles.DiagES; import de.srsoftware.web4rail.tiles.DiagNE; import de.srsoftware.web4rail.tiles.DiagSW; import de.srsoftware.web4rail.tiles.DiagWN; import de.srsoftware.web4rail.tiles.EndE; import de.srsoftware.web4rail.tiles.EndN; import de.srsoftware.web4rail.tiles.EndS; import de.srsoftware.web4rail.tiles.EndW; import de.srsoftware.web4rail.tiles.Eraser; import de.srsoftware.web4rail.tiles.Relay; import de.srsoftware.web4rail.tiles.Shadow; import de.srsoftware.web4rail.tiles.Signal; import de.srsoftware.web4rail.tiles.SignalE; import de.srsoftware.web4rail.tiles.SignalN; import de.srsoftware.web4rail.tiles.SignalS; import de.srsoftware.web4rail.tiles.SignalW; import de.srsoftware.web4rail.tiles.StraightH; import de.srsoftware.web4rail.tiles.StraightV; import de.srsoftware.web4rail.tiles.Tile; import de.srsoftware.web4rail.tiles.Turnout.State; import de.srsoftware.web4rail.tiles.Turnout3E; import de.srsoftware.web4rail.tiles.TurnoutLE; import de.srsoftware.web4rail.tiles.TurnoutLN; import de.srsoftware.web4rail.tiles.TurnoutLS; import de.srsoftware.web4rail.tiles.TurnoutLW; import de.srsoftware.web4rail.tiles.TurnoutRE; import de.srsoftware.web4rail.tiles.TurnoutRN; import de.srsoftware.web4rail.tiles.TurnoutRS; import de.srsoftware.web4rail.tiles.TurnoutRW; /** * This class is a central part of the Application, as it loads, holds and saves all kinds of information: * * @author Stephan Richter, SRSoftware * */ public class Plan extends BaseClass{ /** * The four directions Trains can be within blocks */ public enum Direction{ NORTH, SOUTH, EAST, WEST; public Direction inverse() { switch (this) { case NORTH: return SOUTH; case SOUTH: return NORTH; case EAST: return WEST; case WEST: return EAST; } return null; } } /** * This thread sends a heartbea to the client */ private class Heartbeat extends Thread { @Override public void run() { try { while (true) { sleep(10000); heatbeat(); } } catch (InterruptedException e) { e.printStackTrace(); } } } private static final String ACTION_QR = "qrcode"; public static final String DEFAULT_NAME = "default"; private static final String DIRECTION = "direction"; private static final Logger LOG = LoggerFactory.getLogger(Plan.class); private static final String TILE = "tile"; private static final String X = "x"; private static final String Y = "y"; private static final HashMap clients = new HashMap(); public HashMap tiles = new HashMap(); // The list of tiles of this plan, i.e. the Track layout private HashSet blocks = new HashSet(); // the list of tiles, that are blocks private HashSet signals = new HashSet(); // the list of tiles, that are signals private HashMap routes = new HashMap(); // the list of routes of the track layout private ControlUnit controlUnit = new ControlUnit(this); // the control unit, to which the plan is connected private Contact learningContact; /** * creates a new plan, starts to send heart beats */ public Plan() { new Heartbeat().start(); } /** * manages plan-related commands * @param params the parameters passed from the client * @return Object returned to the client * @throws IOException * @throws ClassNotFoundException * @throws InstantiationException * @throws IllegalAccessException * @throws IllegalArgumentException * @throws InvocationTargetException * @throws NoSuchMethodException * @throws SecurityException */ public Object action(HashMap params) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { switch (params.get(ACTION)) { case ACTION_ADD: return addTile(params.get(TILE),params.get(X),params.get(Y),null); case ACTION_ANALYZE: return analyze(); case ACTION_CLICK: return click(get(params.get(ID),true)); case ACTION_MOVE: return moveTile(params.get(DIRECTION),params.get(ID)); case ACTION_SAVE: return saveTo(DEFAULT_NAME); case ACTION_TIMES: return updateTimes(params); case ACTION_UPDATE: return update(get(params.get(ID),true),params); } return t("Unknown action: {}",params.get(ACTION)); } /** * attaches a new client to the event stream of the plan * @param client */ public void addClient(OutputStreamWriter client) { LOG.debug("Client connected."); clients.put(client, 0); } /** * helper function: creates a list element with a link that will call the clickTile function of the client side javascript. * @param tile the tile a click on which shall be simulated * @param content the text to be displayed to the user * @param list the tag to which the link tag shall be added * @return returns the list element itself * TODO: replace occurences by calls to return request({...});, then remove clickTile from the client javascript */ public static Tag addLink(Tile tile,String content,Tag list) { Tag li = new Tag("li"); new Tag("span").clazz("link").attr("onclick", "return clickTile("+tile.x+","+tile.y+");").content(content).addTo(li).addTo(list); return li; } /** * add a tile of the specified class to the track layout * @param clazz * @param xs * @param ys * @param configJson * @return * @throws ClassNotFoundException * @throws InstantiationException * @throws IllegalAccessException * @throws IllegalArgumentException * @throws InvocationTargetException * @throws NoSuchMethodException * @throws SecurityException * @throws IOException */ private String addTile(String clazz, String xs, String ys, String configJson) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException, IOException { int x = Integer.parseInt(xs); int y = Integer.parseInt(ys); if (clazz == null) throw new NullPointerException(TILE+" must not be null!"); Class tc = Tile.class; clazz = tc.getName().replace(".Tile", "."+clazz); Tile tile = (Tile) tc.getClassLoader().loadClass(clazz).getDeclaredConstructor().newInstance(); if (tile instanceof Eraser) { Tile erased = get(Tile.id(x,y),true); remove(erased); return erased == null ? null : t("Removed {}.",erased); } //if (configJson != null) tile.configure(new JSONObject(configJson)); set(x, y, tile); return t("Added {}",tile.getClass().getSimpleName()); } /** * search all possible routes in the plan * @return a string giving information how many routes have been found */ private String analyze() { Vector routes = new Vector(); for (Block block : blocks) { for (Connector con : block.startPoints()) routes.addAll(follow(new Route().begin(block,con.from.inverse()),con)); } for (Tile tile : tiles.values()) tile.routes().clear(); for (Route route : routes) registerRoute(route.complete()); return t("Found {} routes.",routes.size()); } /** * @return the list of blocks known to the plan, ordered by name */ public Collection blocks() { return new TreeSet(blocks); } /** * calls tile.click() * @param tile * @return * @throws IOException */ private Object click(Tile tile) throws IOException { if (tile == null) return null; return tile.click(); } /** * @return the control unit currently connected to the plan */ public ControlUnit controlUnit() { return controlUnit; } /** * completes a given route during a call to {@link #analyze()}. * It therefore traces where the current part of the route comes from and where it may go. * @param route an incomplete route, that shall be completed * @param connector * @return the set of routes, that result from the tracing operation */ private Collection follow(Route route, Connector connector) { Tile tile = get(Tile.id(connector.x,connector.y),false); Vector results = new Vector<>(); if (tile == null) return results; Tile addedTile = route.add(tile,connector.from); if (addedTile instanceof Block) { Map cons = addedTile.connections(connector.from); LOG.debug("Found {}, coming from {}.",addedTile,connector.from); for (Connector con : cons.keySet()) { // falls direkt nach dem Block noch ein Kontakt kommt: diesen mit zu Route hinzufügen LOG.debug("This is connected to {}",con); Tile nextTile = get(Tile.id(con.x,con.y),false); if (nextTile instanceof Contact) { LOG.debug("{} is followed by {}",addedTile,nextTile); route.add(nextTile, con.from); } break; } return List.of(route); } Map connectors = tile.connections(connector.from); Listroutes = route.multiply(connectors.size()); if (connectors.size()>1) LOG.debug("SPLITTING @ {}",tile); for (Entry entry: connectors.entrySet()) { route = routes.remove(0); connector = entry.getKey(); route.setLast(entry.getValue()); if (connectors.size()>1) LOG.debug("RESUMING from {}",tile); results.addAll(follow(route,connector)); } return results; } /** * returns the tile referenced by the tile id * @param tileId a combination of the coordinates of the requested tile * @param resolveShadows if this is set to true, this function will return the overlaying tiles, if the id belongs to a shadow tile. * @return the tile belonging to the id, or the overlaying tile if the respective tile is a shadow tile. */ public Tile get(String tileId,boolean resolveShadows) { if (isNull(tileId)) return null; Tile tile = tiles.get(tileId); if (resolveShadows && tile instanceof Shadow) tile = ((Shadow)tile).overlay(); return tile; } /** * generates the hardware menu attached to the plan * @return * @throws IOException */ private Tag hardwareMenu() throws IOException { Tag tileMenu = new Tag("div").clazz("hardware").content(t("Hardware")); Tag list = new Tag("div").clazz("list").content(""); new Div(ACTION_POWER).clazz(REALM_CU).content(t("Toggle power")).addTo(list); new Div(ACTION_PROPS).clazz(REALM_CU).content(t("Control unit")).addTo(list); return list.addTo(tileMenu); } /** * prepares the hardware div of the plan * @return */ private Tag heartbeat() { return new Div("heartbeat").content(""); } /** * send a heatbeat to the client */ public void heatbeat() { stream("heartbeat @ "+new Date().getTime()); } private Tag help() { Tag help = new Tag("div").clazz("help").content(t("Help")); Tag list = new Tag("div").clazz("list").content(""); new Tag("div").content(t("Online Documentation")).attr("onclick", "window.open('"+GITHUB_URL+"')").addTo(list); new Tag("div").content(t("Report Issue")).attr("onclick", "window.open('"+GITHUB_URL+"/issues')").addTo(list); return list.addTo(help); } /** * generates a html document of this plan * @return * @throws IOException */ public Page html() throws IOException { Page page = new Page().append("
"); for (Tile tile: tiles.values()) { if (tile == null) continue; page.append("\t\t"+tile.tag(null)+"\n"); } return page .append("
") .append(menu()) .append(messages()) .append(heartbeat()) .append("
") .style("css/style.css") .js("js/jquery-3.5.1.min.js") .js("js/plan.js"); } public void learn(Contact contact) { learningContact = contact; LOG.debug("learning contact {}",learningContact); } /** * loads a track layout from a file, along with its assigned cars, trains, routes and control unit settings * @param filename * @return * @throws IOException * @throws ClassNotFoundException * @throws InstantiationException * @throws IllegalAccessException * @throws IllegalArgumentException * @throws InvocationTargetException * @throws NoSuchMethodException * @throws SecurityException */ public static void load(String filename) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { plan = new Plan(); try { Car.loadAll(filename+".cars",plan); } catch (Exception e) { LOG.warn("Was not able to load cars!",e); } Tile.loadAll(filename+".plan",plan); try { Train.loadAll(filename+".trains",plan); } catch (Exception e) { LOG.warn("Was not able to load trains!",e); } try { Route.loadAll(filename+".routes",plan); } catch (Exception e) { LOG.warn("Was not able to load routes!",e); } try { plan.controlUnit.load(filename+".cu"); } catch (Exception e) { LOG.warn("Was not able to load control unit settings!",e); } try { plan.controlUnit.start(); } catch (Exception e) { LOG.warn("Was not able to establish connection to control unit!"); } } /** * creates the main menu attached to the plan * @return * @throws IOException */ private Tag menu() throws IOException { Tag menu = new Tag("div").clazz("menu"); new Tag("div").clazz("emergency").content(t("Emergency")).attr("onclick","return request({realm:'"+REALM_CU+"',action:'"+ACTION_EMERGENCY+"'});").addTo(menu); moveMenu().addTo(menu); planMenu().addTo(menu); hardwareMenu().addTo(menu); tileMenu().addTo(menu); trainMenu().addTo(menu); help().addTo(menu); return menu; } /** * prepares the messages div of the plan * @return */ private Tag messages() { return new Div("messages").content(""); } /** * creates the move-tile menu of the plan * @return */ private Tag moveMenu() { Tag tileMenu = new Tag("div").clazz("move").title(t("Move tiles")).content(t("↹")); Tag tiles = new Tag("div").clazz("list").content(""); new Div("west").title(t("Move west")).content("↤").addTo(tiles); new Div("east").title(t("Move east")).content("↦").addTo(tiles); new Div("north").title(t("Move north")).content("↥").addTo(tiles); new Div("south").title(t("Move south")).content("↧").addTo(tiles); return tiles.addTo(tileMenu); } /** * processes move-tile instructions sent from the client * @param direction * @param tileId * @return * @throws NumberFormatException * @throws IOException */ private String moveTile(String direction, String tileId) throws NumberFormatException, IOException { switch (direction) { case "south": return moveTile(get(tileId,false),Direction.SOUTH); case "north": return moveTile(get(tileId,false),Direction.NORTH); case "east": return moveTile(get(tileId,false),Direction.EAST); case "west": return moveTile(get(tileId,false),Direction.WEST); } throw new InvalidParameterException(t("\"{}\" is not a known direction!")); } /** * processes move-tile instructions sent from the client (subroutine) * @param tile * @param direction * @return * @throws IOException */ private String moveTile(Tile tile, Direction direction) throws IOException { boolean moved = false; if (tile != null) { LOG.debug("moveTile({},{},{})",direction,tile.x,tile.y); switch (direction) { case EAST: moved = moveTile(tile,+1,0); break; case WEST: moved = moveTile(tile,-1,0); break; case NORTH: moved = moveTile(tile,0,-1); break; case SOUTH: moved = moveTile(tile,0,+1); break; } } return t(moved ? "Tile(s) moved.":"No tile(s) moved."); } /** * processes move-tile instructions sent from the client (subroutine) * @param tile * @param xstep * @param ystep * @return * @throws IOException */ private boolean moveTile(Tile tile,int xstep,int ystep) throws IOException { LOG.error("moveTile({} +{}/+{})",tile,xstep,ystep); Stack stack = new Stack(); while (tile != null) { LOG.debug("scheduling tile for movement: {}",tile); stack.add(tile); tile = get(Tile.id(tile.x+xstep, tile.y+ystep),false); } while (!stack.isEmpty()) { tile = stack.pop(); if (!(tile instanceof Shadow)) { LOG.debug("altering position of {}",tile); remove(tile); set(tile.x+xstep,tile.y+ystep,tile); } } return false; } /** * adds a new tile to the plan on the client side * @param tile * @return * @throws IOException */ public Tile place(Tile tile) { try { stream("place "+tile.tag(null)); } catch (IOException e) { e.printStackTrace(); } return tile; } /** * generates the action menu that is appended to the plan * @return * @throws IOException */ private Tag planMenu() throws IOException { Tag actionMenu = new Tag("div").clazz("actions").content(t("Plan")); Tag actions = new Tag("div").clazz("list").content(""); new Div(ACTION_SAVE).clazz(REALM_PLAN).content(t("Save")).addTo(actions); new Div(ACTION_ANALYZE).clazz(REALM_PLAN).content(t("Analyze")).addTo(actions); new Div(ACTION_QR).clazz(REALM_PLAN).content(t("QR-Code")).addTo(actions); return actions.addTo(actionMenu); } /** * adds a command to the control unit's command queue * @param command * @return */ public Command queue(Command command) { return controlUnit.queue(command); } /** * adds a new route to the plan * @param newRoute * @return */ Route registerRoute(Route newRoute) { for (Tile tile: newRoute.path()) { if (isSet(tile)) tile.add(newRoute); } int routeId = newRoute.id(); Route existingRoute = routes.get(routeId); if (isSet(existingRoute)) newRoute.addPropertiesFrom(existingRoute); routes.put(routeId, newRoute); return newRoute; } /** * removes a tile from the track layout * @param tile */ private void remove(Tile tile) { if (isNull(tile)) return; removeTile(tile.x,tile.y); if (tile instanceof Block) blocks.remove(tile); for (int i=1; i params) { String[] parts = params.get(CONTEXT).split(":"); String realm = parts[0]; String id = parts.length>1 ? parts[1] : null; switch (realm) { case REALM_ROUTE: return route(Integer.parseInt(id)).properties(params); case REALM_PLAN: Tile tile = get(id, false); return tile == null? null : tile.propMenu(); } return null; } public SortedSet signals() { return new TreeSet(signals); } /** * sends some data to the clients * @param data */ public synchronized void stream(String data) { data = data.replaceAll("\n", "").replaceAll("\r", ""); //if (!data.startsWith("heartbeat")) LOG.debug("streaming: {}",data); Vector badClients = null; for (Entry entry : clients.entrySet()) { OutputStreamWriter client = entry.getKey(); try { client.write("data: "+data+"\n\n"); client.flush(); clients.put(client,0); } catch (IOException e) { int errorCount = entry.getValue()+1; LOG.info("Error #{} on client: {}",errorCount,e.getMessage()); if (errorCount > 4) { if (isNull(badClients)) badClients = new Vector(); try { client.close(); } catch (IOException e1) {} badClients.add(client); } else clients.put(client,errorCount); } } if (badClients != null) for (OutputStreamWriter client: badClients) { LOG.info("Disconnecting client."); clients.remove(client); } } /** * shorthand for Translations.get(message,fills) * @param message * @param fills * @return */ private String t(String message, Object...fills) { return Translation.get(Application.class, message, fills); } /** * generates the menu for selecting tiles to be added to the layout * @return * @throws IOException */ private Tag tileMenu() throws IOException { Tag tileMenu = new Tag("div").clazz("addtile").title(t("Add tile")).content("╦"); Tag tiles = new Tag("div").clazz("list").content(""); new CrossV().tag(null).addTo(tiles); new CrossH().tag(null).addTo(tiles); new StraightH().tag(null).addTo(tiles); new StraightV().tag(null).addTo(tiles); new ContactH().tag(null).addTo(tiles); new ContactV().tag(null).addTo(tiles); new SignalW().tag(null).addTo(tiles); new SignalE().tag(null).addTo(tiles); new SignalS().tag(null).addTo(tiles); new SignalN().tag(null).addTo(tiles); new BlockH().tag(null).addTo(tiles); new BlockV().tag(null).addTo(tiles); new DiagES().tag(null).addTo(tiles); new DiagSW().tag(null).addTo(tiles); new DiagNE().tag(null).addTo(tiles); new DiagWN().tag(null).addTo(tiles); new EndE().tag(null).addTo(tiles); new EndW().tag(null).addTo(tiles); new EndN().tag(null).addTo(tiles); new EndS().tag(null).addTo(tiles); new TurnoutRS().tag(null).addTo(tiles); new TurnoutRN().tag(null).addTo(tiles); new TurnoutRW().tag(null).addTo(tiles); new TurnoutRE().tag(null).addTo(tiles); new TurnoutLN().tag(null).addTo(tiles); new TurnoutLS().tag(null).addTo(tiles); new TurnoutLW().tag(null).addTo(tiles); new TurnoutLE().tag(null).addTo(tiles); new Turnout3E().tag(null).addTo(tiles); new Relay().setLabel(true,"RL").tag(null).addTo(tiles); new Contact().tag(null).addTo(tiles); new Eraser().tag(null).addTo(tiles); return tiles.addTo(tileMenu); } /** * generates the train menu * @return * @throws IOException */ private Tag trainMenu() throws IOException { Tag tileMenu = new Tag("div").clazz("trains").content(t("Trains")); Tag tiles = new Tag("div").clazz("list").content(""); new Div(ACTION_PROPS).clazz(REALM_TRAIN).content(t("Manage trains")).addTo(tiles); new Div(ACTION_PROPS).clazz(REALM_LOCO).content(t("Manage locos")).addTo(tiles); new Div(ACTION_PROPS).clazz(REALM_CAR).content(t("Manage cars")).addTo(tiles); return tiles.addTo(tileMenu); } /** * updates a tile * @param tile * @param params * @return * @throws IOException */ private Tile update(Tile tile, HashMap params) throws IOException { return tile == null ? null : tile.update(params); } private Object updateTimes(HashMap params) throws IOException { Tile tile = get(params.get(ID),false); if (tile instanceof Block) { Block block = (Block) tile; place(block.updateTimes(params)); return tile.propMenu(); } return t("updateTimes called on non-block tile!"); } /** * sends a Ghost train warning to the client * @param contact */ public void warn(Contact contact) { stream(t("Warning: {}",t("Ghost train @ {}",contact))); } }