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.
550 lines
19 KiB
550 lines
19 KiB
package de.srsoftware.web4rail; |
|
|
|
import java.io.BufferedReader; |
|
import java.io.BufferedWriter; |
|
import java.io.File; |
|
import java.io.FileReader; |
|
import java.io.FileWriter; |
|
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.Stack; |
|
import java.util.Vector; |
|
|
|
import org.json.JSONObject; |
|
import org.slf4j.Logger; |
|
import org.slf4j.LoggerFactory; |
|
|
|
import de.keawe.tools.translations.Translation; |
|
import de.srsoftware.tools.Tag; |
|
import de.srsoftware.web4rail.tiles.Block; |
|
import de.srsoftware.web4rail.tiles.BlockH; |
|
import de.srsoftware.web4rail.tiles.BlockV; |
|
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.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; |
|
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; |
|
|
|
public class Plan { |
|
public enum Direction{ |
|
NORTH, SOUTH, EAST, WEST |
|
} |
|
|
|
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 = "action"; |
|
private static final String ACTION_ADD = "add"; |
|
private static final String ACTION_ANALYZE = "analyze"; |
|
private static final String ACTION_MOVE = "move"; |
|
private static final String ACTION_PROPS = "openProps"; |
|
private static final String ACTION_SAVE = "save"; |
|
private static final String ACTION_UPDATE = "update"; |
|
private static final String TILE = "tile"; |
|
private static final Logger LOG = LoggerFactory.getLogger(Plan.class); |
|
private static final String X = "x"; |
|
private static final String Y = "y"; |
|
private static final String FILE = "file"; |
|
private static final String DIRECTION = "direction"; |
|
private static final String ACTION_ROUTE = "openRoute"; |
|
private static final String ID = "id"; |
|
private static final String ROUTE = "route"; |
|
private static final HashMap<OutputStreamWriter,Integer> clients = new HashMap<OutputStreamWriter, Integer>(); |
|
|
|
private HashMap<Integer,HashMap<Integer,Tile>> tiles = new HashMap<Integer,HashMap<Integer,Tile>>(); |
|
private HashSet<Block> blocks = new HashSet<Block>(); |
|
private HashMap<String, Route> routes = new HashMap<String, Route>(); |
|
|
|
public Plan() { |
|
new Heartbeat().start(); |
|
} |
|
|
|
private Tag actionMenu() throws IOException { |
|
Tag tileMenu = new Tag("div").clazz("actions").content(t("Actions")); |
|
StringBuffer tiles = new StringBuffer(); |
|
tiles.append(new Tag("div").id("save").content(t("Save plan"))); |
|
tiles.append(new Tag("div").id("analyze").content(t("Analyze plan"))); |
|
return new Tag("div").clazz("list").content(tiles.toString()).addTo(tileMenu); |
|
} |
|
|
|
public void addClient(OutputStreamWriter client) { |
|
LOG.debug("Client connected."); |
|
clients.put(client, 0); |
|
} |
|
|
|
public static void addLink(Tile tile,String content,Tag list) { |
|
new Tag("li").clazz("link").attr("onclick", "return clickTile("+tile.x+","+tile.y+");").content(content).addTo(list); |
|
} |
|
|
|
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<Tile> 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(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()); |
|
} |
|
|
|
private String analyze() { |
|
Vector<Route> routes = new Vector<Route>(); |
|
for (Block block : blocks) { |
|
for (Connector con : block.startPoints()) routes.addAll(follow(new Route().start(block),con)); |
|
} |
|
this.routes.clear(); |
|
for (HashMap<Integer, Tile> column: tiles.values()) { |
|
for (Tile tile : column.values()) tile.routes().clear(); |
|
} |
|
for (Route route : routes) registerRoute(route); |
|
return t("Found {} routes.",routes.size()); |
|
} |
|
|
|
public Collection<Block> blocks() { |
|
return blocks; |
|
} |
|
|
|
private Collection<Route> follow(Route route, Connector connector) { |
|
Tile tile = get(connector.x,connector.y,false); |
|
Vector<Route> results = new Vector<>(); |
|
if (tile == null) return results; |
|
Tile addedTile = route.add(tile,connector.from); |
|
if (addedTile instanceof Block) return List.of(route); |
|
Map<Connector, State> connectors = tile.connections(connector.from); |
|
List<Route>routes = route.multiply(connectors.size()); |
|
LOG.debug("{}",tile); |
|
if (connectors.size()>1) LOG.debug("SPLITTING @ {}",tile); |
|
|
|
for (Entry<Connector, State> entry: connectors.entrySet()) { |
|
route = routes.remove(0); |
|
connector = entry.getKey(); |
|
State state = entry.getValue(); |
|
route.setLast(state); |
|
if (connectors.size()>1) { |
|
LOG.debug("RESUMING from {}",tile); |
|
} |
|
results.addAll(follow(route,connector)); |
|
} |
|
|
|
return results; |
|
} |
|
|
|
public Tile get(int x, int y,boolean resolveShadows) { |
|
HashMap<Integer, Tile> column = tiles.get(x); |
|
Tile tile = (column == null) ? null : column.get(y); |
|
if (resolveShadows && tile instanceof Shadow) tile = ((Shadow)tile).overlay(); |
|
return tile; |
|
} |
|
|
|
private Tag heartbeat() { |
|
return new Tag("div").id("heartbeat").content(""); |
|
} |
|
|
|
public void heatbeat() { |
|
stream("heartbeat @ "+new Date().getTime()); |
|
} |
|
|
|
public Page html() throws IOException { |
|
Page page = new Page().append("<div id=\"plan\">"); |
|
for (Entry<Integer, HashMap<Integer, Tile>> column : tiles.entrySet()) { |
|
int x = column.getKey(); |
|
for (Entry<Integer, Tile> row : column.getValue().entrySet()) { |
|
int y = row.getKey(); |
|
Tile tile = row.getValue().position(x, y); |
|
if (tile == null) continue; |
|
page.append("\t\t"+tile.tag(null)+"\n"); |
|
} |
|
} |
|
return page |
|
.append(menu()) |
|
.append(messages()) |
|
.append(heartbeat()) |
|
.append("</div>") |
|
.style("css/style.css") |
|
.js("js/jquery-3.5.1.min.js") |
|
.js("js/plan.js"); |
|
} |
|
|
|
public static Plan load(String filename) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { |
|
Plan result = new Plan(); |
|
File file = new File(filename+".plan"); |
|
BufferedReader br = new BufferedReader(new FileReader(file)); |
|
while (br.ready()) { |
|
String line = br.readLine().trim(); |
|
String[] parts = line.split(":",4); |
|
try { |
|
String x = parts[0]; |
|
String y = parts[1]; |
|
String clazz = parts[2]; |
|
result.addTile(clazz, x, y, parts.length>3 ? parts[3] : null); |
|
} catch (Exception e) { |
|
LOG.warn("Was not able to load \"{}\":",line,e); |
|
} |
|
} |
|
br.close(); |
|
file = new File(filename+".routes"); |
|
if (file.exists()) { |
|
br = new BufferedReader(new FileReader(file)); |
|
while (br.ready()) { |
|
String line = br.readLine().trim(); |
|
String[] parts = line.split("=",2); |
|
try { |
|
//String id = parts[0]; |
|
JSONObject json = new JSONObject(parts[1]); |
|
Route route = new Route(); |
|
json.getJSONArray(Route.PATH).forEach(entry -> { |
|
JSONObject pos = (JSONObject) entry; |
|
Tile tile = result.get(pos.getInt("x"),pos.getInt("y"),false); |
|
if (route.path().isEmpty()) { |
|
route.start((Block) tile); |
|
} else { |
|
route.add(tile, null); |
|
} |
|
}); |
|
json.getJSONArray(Route.SIGNALS).forEach(entry -> { |
|
JSONObject pos = (JSONObject) entry; |
|
Tile tile = result.get(pos.getInt("x"),pos.getInt("y"),false); |
|
route.addSignal((Signal) tile); |
|
}); |
|
json.getJSONArray(Route.TURNOUTS).forEach(entry -> { |
|
JSONObject pos = (JSONObject) entry; |
|
Tile tile = result.get(pos.getInt("x"),pos.getInt("y"),false); |
|
route.addTurnout((Turnout) tile, Turnout.State.valueOf(pos.getString(Turnout.STATE))); |
|
}); |
|
if (json.has(Route.NAME)) route.name(json.getString(Route.NAME)); |
|
result.registerRoute(route); |
|
} catch (Exception e) { |
|
LOG.warn("Was not able to load \"{}\":",line,e); |
|
} |
|
} |
|
br.close(); |
|
} else LOG.debug("{} not found.",file); |
|
return result; |
|
} |
|
|
|
private Tag menu() throws IOException { |
|
Tag menu = new Tag("div").clazz("menu"); |
|
actionMenu().addTo(menu); |
|
moveMenu().addTo(menu); |
|
tileMenu().addTo(menu); |
|
return menu; |
|
} |
|
|
|
private Tag messages() { |
|
return new Tag("div").id("messages").content(""); |
|
} |
|
|
|
private Tag moveMenu() { |
|
Tag tileMenu = new Tag("div").clazz("move").title(t("Move tiles")).content(t("↹")); |
|
StringBuffer tiles = new StringBuffer(); |
|
tiles.append(new Tag("div").id("west").title(t("Move west")).content("↤")); |
|
tiles.append(new Tag("div").id("east").title(t("Move east")).content("↦")); |
|
tiles.append(new Tag("div").id("north").title(t("Move north")).content("↥")); |
|
tiles.append(new Tag("div").id("south").title(t("Move south")).content("↧")); |
|
return new Tag("div").clazz("list").content(tiles.toString()).addTo(tileMenu); |
|
} |
|
|
|
private String moveTile(String direction, String x, String y) throws NumberFormatException, IOException { |
|
switch (direction) { |
|
case "south": |
|
return moveTile(Direction.SOUTH,Integer.parseInt(x),Integer.parseInt(y)); |
|
case "north": |
|
return moveTile(Direction.NORTH,Integer.parseInt(x),Integer.parseInt(y)); |
|
case "east": |
|
return moveTile(Direction.EAST,Integer.parseInt(x),Integer.parseInt(y)); |
|
case "west": |
|
return moveTile(Direction.WEST,Integer.parseInt(x),Integer.parseInt(y)); |
|
} |
|
throw new InvalidParameterException(t("\"{}\" is not a known direction!")); |
|
} |
|
|
|
private String moveTile(Direction direction, int x, int y) throws IOException { |
|
//LOG.debug("moveTile({},{},{})",direction,x,y); |
|
boolean moved = false; |
|
switch (direction) { |
|
case EAST: |
|
moved = moveTile(x,y,+1,0); |
|
break; |
|
case WEST: |
|
moved = moveTile(x,y,-1,0); |
|
break; |
|
case NORTH: |
|
moved = moveTile(x,y,0,-1); |
|
break; |
|
case SOUTH: |
|
moved = moveTile(x,y,0,+1); |
|
break; |
|
} |
|
return t(moved ? "Tile(s) moved.":"No tile(s) moved."); |
|
} |
|
|
|
private boolean moveTile(int x, int y,int xstep,int ystep) throws IOException { |
|
LOG.error("moveTile({}+ {},{}+ {}) not implemented",x,xstep,y,ystep); |
|
Stack<Tile> stack = new Stack<Tile>(); |
|
Tile tile = get(x,y,false); |
|
while (tile != null) { |
|
LOG.debug("scheduling tile for movement: {} @ {},{}",tile,x,y); |
|
stack.add(tile); |
|
x+=xstep; |
|
y+=ystep; |
|
tile = get(x,y,false); |
|
} |
|
while (!stack.isEmpty()) { |
|
tile = stack.pop(); |
|
if (!(tile instanceof Shadow)) { |
|
remove(tile); |
|
set(tile.x+xstep,tile.y+ystep,tile); |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
public Object process(HashMap<String, String> params) { |
|
try { |
|
String action = params.get(ACTION); |
|
|
|
if (action == null) throw new NullPointerException(ACTION+" should not be null!"); |
|
switch (action) { |
|
case ACTION_ADD: |
|
return addTile(params.get(TILE),params.get(X),params.get(Y),null); |
|
case ACTION_ANALYZE: |
|
return analyze(); |
|
case ACTION_MOVE: |
|
return moveTile(params.get(DIRECTION),params.get(X),params.get(Y)); |
|
case ACTION_PROPS: |
|
return propMenu(params.get(X),params.get(Y)); |
|
case ACTION_ROUTE: |
|
return routeProperties(params.get(ID)); |
|
case ACTION_SAVE: |
|
return saveTo(params.get(FILE)); |
|
case ACTION_UPDATE: |
|
return update(params); |
|
default: |
|
LOG.warn("Unknown action: {}",action); |
|
} |
|
return t("Unknown action: {}",action); |
|
} catch (Exception e) { |
|
String msg = e.getMessage(); |
|
if (msg == null || msg.isEmpty()) msg = t("An unknown error occured!"); |
|
return msg; |
|
} |
|
} |
|
|
|
private Object routeProperties(String routeId) { |
|
Route route = routes.get(routeId); |
|
if (route == null) return t("Could not find route \"{}\"",routeId); |
|
return route.properties(); |
|
} |
|
|
|
private Tag propMenu(String x, String y) { |
|
return propMenu(Integer.parseInt(x),Integer.parseInt(y)); |
|
} |
|
|
|
private Tag propMenu(int x, int y) { |
|
Tile tile = get(x, y,true); |
|
if (tile == null) return null; |
|
return tile.propMenu(); |
|
} |
|
|
|
private void registerRoute(Route route) { |
|
for (Tile tile: route.path()) tile.add(route); |
|
routes.put(route.id(), route); |
|
} |
|
|
|
|
|
|
|
private void remove(Tile tile) { |
|
remove_intern(tile.x,tile.y); |
|
if (tile instanceof Block) blocks.remove(tile); |
|
for (int i=1; i<tile.len(); i++) remove_intern(tile.x+i, tile.y); // remove shadow tiles |
|
for (int i=1; i<tile.height(); i++) remove_intern(tile.x, tile.y+i); // remove shadow tiles |
|
if (tile != null) stream("remove tile-"+tile.x+"-"+tile.y); |
|
} |
|
|
|
private void remove_intern(int x, int y) { |
|
HashMap<Integer, Tile> column = tiles.get(x); |
|
if (column != null) column.remove(y); |
|
} |
|
|
|
private String saveTo(String name) throws IOException { |
|
if (name == null || name.isEmpty()) throw new NullPointerException("Name must not be empty!"); |
|
File file = new File(name+".plan"); |
|
BufferedWriter br = new BufferedWriter(new FileWriter(file)); |
|
for (Entry<Integer, HashMap<Integer, Tile>> column : tiles.entrySet()) { |
|
int x = column.getKey(); |
|
for (Entry<Integer, Tile> row : column.getValue().entrySet()) { |
|
int y = row.getKey(); |
|
Tile tile = row.getValue().position(x, y); |
|
if (tile != null && !(tile instanceof Shadow)) { |
|
br.append(x+":"+y+":"+tile.getClass().getSimpleName()); |
|
JSONObject config = tile.config(); |
|
if (!config.isEmpty()) br.append(":"+config); |
|
br.append("\n"); |
|
} |
|
} |
|
} |
|
br.close(); |
|
file = new File(name+".routes"); |
|
br = new BufferedWriter(new FileWriter(file)); |
|
for (Route route: routes.values()) { |
|
br.append(route.id()+"="+route.json()+"\n"); |
|
} |
|
br.close(); |
|
return t("Plan saved as \"{}\".",file); |
|
} |
|
|
|
public void set(int x,int y,Tile tile) throws IOException { |
|
if (tile == null) return; |
|
if (tile instanceof Block) blocks.add((Block) tile); |
|
for (int i=1; i<tile.len(); i++) set(x+i,y,new Shadow(tile)); |
|
for (int i=1; i<tile.height(); i++) set(x,y+i,new Shadow(tile)); |
|
set_intern(x,y,tile); |
|
stream("place "+tile.tag(null)); |
|
} |
|
|
|
private void set_intern(int x, int y, Tile tile) { |
|
HashMap<Integer, Tile> column = tiles.get(x); |
|
if (column == null) { |
|
column = new HashMap<Integer, Tile>(); |
|
tiles.put(x,column); |
|
} |
|
column.put(y,tile.position(x, y)); |
|
} |
|
|
|
private synchronized void stream(String data) { |
|
data = data.replaceAll("\n", "").replaceAll("\r", ""); |
|
LOG.debug("streaming: {}",data); |
|
Vector<OutputStreamWriter> badClients = null; |
|
for (Entry<OutputStreamWriter, Integer> 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 (badClients == null) badClients = new Vector<OutputStreamWriter>(); |
|
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); |
|
} |
|
} |
|
|
|
private String t(String message, Object...fills) { |
|
return Translation.get(Application.class, message, fills); |
|
} |
|
|
|
private Tag tileMenu() throws IOException { |
|
Tag tileMenu = new Tag("div").clazz("addtile").title(t("Add tile")).content("◫"); |
|
|
|
StringBuffer tiles = new StringBuffer(); |
|
tiles.append(new StraightH().tag(null)); |
|
tiles.append(new StraightV().tag(null)); |
|
tiles.append(new ContactH().tag(null)); |
|
tiles.append(new ContactV().tag(null)); |
|
tiles.append(new SignalW().tag(null)); |
|
tiles.append(new SignalE().tag(null)); |
|
tiles.append(new SignalS().tag(null)); |
|
tiles.append(new SignalN().tag(null)); |
|
tiles.append(new BlockH().tag(null)); |
|
tiles.append(new BlockV().tag(null)); |
|
tiles.append(new DiagES().tag(null)); |
|
tiles.append(new DiagSW().tag(null)); |
|
tiles.append(new DiagNE().tag(null)); |
|
tiles.append(new DiagWN().tag(null)); |
|
tiles.append(new EndE().tag(null)); |
|
tiles.append(new EndW().tag(null)); |
|
tiles.append(new EndN().tag(null)); |
|
tiles.append(new EndS().tag(null)); |
|
tiles.append(new TurnoutRS().tag(null)); |
|
tiles.append(new TurnoutRN().tag(null)); |
|
tiles.append(new TurnoutRW().tag(null)); |
|
tiles.append(new TurnoutRE().tag(null)); |
|
tiles.append(new TurnoutLN().tag(null)); |
|
tiles.append(new TurnoutLS().tag(null)); |
|
tiles.append(new TurnoutLW().tag(null)); |
|
tiles.append(new TurnoutLE().tag(null)); |
|
tiles.append(new Turnout3E().tag(null)); |
|
tiles.append(new CrossH().tag(null)); |
|
tiles.append(new CrossV().tag(null)); |
|
tiles.append(new Eraser().tag(null)); |
|
return new Tag("div").clazz("list").content(tiles.toString()).addTo(tileMenu); |
|
} |
|
|
|
private Object update(HashMap<String, String> params) throws IOException { |
|
if (params.containsKey(ROUTE)) { |
|
Route route = routes.get(params.get(ROUTE)); |
|
if (route == null) return t("Unknown route: {}",params.get(ROUTE)); |
|
route.update(params); |
|
} else update(Integer.parseInt(params.get("x")),Integer.parseInt(params.get("y")),params); |
|
return this.html(); |
|
} |
|
|
|
private void update(int x,int y, HashMap<String, String> params) throws IOException { |
|
Tile tile = get(x,y,true); |
|
if (tile != null) set(x,y,tile.update(params)); |
|
} |
|
}
|
|
|