diff --git a/pom.xml b/pom.xml index 02cce05..dcd0e72 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 de.srsoftware web4rail - 1.4.6 + 1.4.7 Web4Rail jar Java Model Railway Control diff --git a/resources/translations/Application.de.translation b/resources/translations/Application.de.translation index 15e63fc..841bbda 100644 --- a/resources/translations/Application.de.translation +++ b/resources/translations/Application.de.translation @@ -326,6 +326,7 @@ Start actions : Start-Aktionen Stock ID : Inventarnummer Stop settings : Halte-Einstellungen Start autopilot : Autopilot starten +Start delay : Start-Verzögerung Started {} : {} gestartet starting delay : Anfahrverzögerung State : Status diff --git a/src/main/java/de/srsoftware/web4rail/Range.java b/src/main/java/de/srsoftware/web4rail/Range.java index eb7bae6..100c6e8 100644 --- a/src/main/java/de/srsoftware/web4rail/Range.java +++ b/src/main/java/de/srsoftware/web4rail/Range.java @@ -13,8 +13,18 @@ public class Range extends BaseClass{ private static final String MAX = "max"; private static final String MIN = "min"; - public int min=0,max=10000; + public int min,max; + public Range(int min, int max) { + this.min = min; + this.max = max; + validate(); + } + + public Range() { + this(0,10_000); + } + public JSONObject json() { return new JSONObject(Map.of(MIN,min,MAX,max)); } diff --git a/src/main/java/de/srsoftware/web4rail/Route.java b/src/main/java/de/srsoftware/web4rail/Route.java index 8fe00a2..376ad3b 100644 --- a/src/main/java/de/srsoftware/web4rail/Route.java +++ b/src/main/java/de/srsoftware/web4rail/Route.java @@ -22,7 +22,6 @@ import de.srsoftware.web4rail.Plan.Direction; import de.srsoftware.web4rail.actions.Action; import de.srsoftware.web4rail.actions.ActionList; import de.srsoftware.web4rail.actions.BrakeStart; -import de.srsoftware.web4rail.actions.DelayedAction; import de.srsoftware.web4rail.actions.FinishRoute; import de.srsoftware.web4rail.actions.PreserveRoute; import de.srsoftware.web4rail.actions.SetSignal; @@ -76,6 +75,12 @@ public class Route extends BaseClass { private static final String ROUTE_SETUP = "route_setup"; + private static final String MIN_START_DELAY = "min_start_delay"; + + private static final String MAX_START_DELAY = "max_start_delay"; + + private static final String STARt_DELAY = "start_delay"; + private static HashMap names = new HashMap(); // maps id to name. needed to keep names during plan.analyze() private HashMap brakeTimes = new HashMap(); @@ -85,17 +90,17 @@ public class Route extends BaseClass { private boolean disabled = false; private Block endBlock = null; public Direction endDirection; + private Route nextPreparedRoute = null; + private RoutePrepper nextRoutePrepper = null; private Vector path; private Vector signals; private HashMap triggeredActions = new HashMap(); private HashMap turnouts; private Block startBlock = null; + private Range startDelay = null; public Direction startDirection; private HashSet triggeredContacts = new HashSet<>(); - private Route nextPreparedRoute; - - private RoutePrepper nextRoutePrepper; public Route() { conditions = new ConditionList(); @@ -303,12 +308,8 @@ public class Route extends BaseClass { add(ROUTE_SETUP,new SetTurnout(this).setTurnout(turnout).setState(state)); } for (Signal signal : signals) add(ROUTE_SETUP,new SetSignal(this).set(signal).to(Signal.GREEN)); - if (signals.isEmpty()) { - add(ROUTE_START,new SetSpeed(this).to(999)); - } else { - DelayedAction da = new DelayedAction(this).setMinDelay(1000).setMaxDelay(7500); - add(ROUTE_START,da.add(new SetSpeed(this).to(999))); - } + startDelay = signals.isEmpty() ? new Range(0,0) : new Range(1000,7500); + add(ROUTE_START,new SetSpeed(this).to(999)); return this; } @@ -485,6 +486,7 @@ public class Route extends BaseClass { if (isSet(name)) json.put(NAME, name); if (disabled) json.put(DISABLED, true); + if (isSet(startDelay)) json.put(STARt_DELAY, startDelay.json()); return json; } @@ -618,6 +620,7 @@ public class Route extends BaseClass { JSONObject dummy = json.getJSONObject(BRAKE_TIMES); dummy.keySet().forEach(key -> brakeTimes.put(key, dummy.getInt(key))); } + if (json.has(STARt_DELAY)) startDelay = new Range().load(json.getJSONObject(STARt_DELAY)); return plan.registerRoute(this); } @@ -725,7 +728,12 @@ public class Route extends BaseClass { formInputs.add(t("Name"),nameSpan); Checkbox checkbox = new Checkbox(DISABLED, t("disabled"), disabled); if (disabled) checkbox.clazz("disabled"); - formInputs.add(t("State"),checkbox); + formInputs.add(t("State"),checkbox); + + Tag span = new Tag("span"); + new Input(MIN_START_DELAY,isSet(startDelay) ? startDelay.min : 0).numeric().addTo(span).content(" ... "); + new Input(MAX_START_DELAY,isSet(startDelay) ? startDelay.max : 0).numeric().addTo(span).content(" ms"); + formInputs.add(t("Start delay"),span); postForm.add(basicProperties()); if (!turnouts.isEmpty()) postForm.add(turnouts()); @@ -846,9 +854,10 @@ public class Route extends BaseClass { if (parts.length>1) name(parts[0].trim()+" - "+parts[parts.length-1].trim()); return this; } - - public boolean start() { - LOG.debug("{}.start()",this); + + public boolean startNow() { + LOG.debug("{}.startNow()",this); + if (isNull(context) || context.invalidated()) { LOG.debug("Invalid context: {}",context); return false; @@ -878,6 +887,11 @@ public class Route extends BaseClass { return true; } + public boolean start() { + if (isSet(startDelay)) sleep(startDelay.random()); + return startNow(); + } + public Block startBlock() { return startBlock; } @@ -916,11 +930,35 @@ public class Route extends BaseClass { disabled = "on".equals(params.get(DISABLED)); + String delay = params.get(MIN_START_DELAY); + if (isSet(delay)) try { + int min = Integer.parseInt(delay); + if (isNull(startDelay)) { + startDelay = new Range(min, min); + } else { + startDelay.min = min; + startDelay.validate(); + } + } catch (NumberFormatException e) {} + + delay = params.get(MAX_START_DELAY); + if (isSet(delay)) try { + int max = Integer.parseInt(delay); + if (isNull(startDelay)) { + startDelay = new Range(max, max); + } else { + startDelay.max = max; + startDelay.validate(); + } + } catch (NumberFormatException e) {} + + Condition condition = Condition.create(params.get(REALM_CONDITION)); if (isSet(condition)) { condition.parent(this); conditions.add(condition); } + super.update(params); return properties(); } diff --git a/src/main/java/de/srsoftware/web4rail/actions/SetTurnout.java b/src/main/java/de/srsoftware/web4rail/actions/SetTurnout.java index dcdf36f..e72e6af 100644 --- a/src/main/java/de/srsoftware/web4rail/actions/SetTurnout.java +++ b/src/main/java/de/srsoftware/web4rail/actions/SetTurnout.java @@ -28,7 +28,7 @@ public class SetTurnout extends Action { public boolean fire(Context context,Object cause) { if (context.invalidated()) return false; if (isNull(turnout)) return false; - if (!turnout.state(state).succeeded()) return false; + if (!turnout.state(state,false).succeeded()) return false; if (turnout.address() == 0) return true; sleep(1000); return true; diff --git a/src/main/java/de/srsoftware/web4rail/moving/Train.java b/src/main/java/de/srsoftware/web4rail/moving/Train.java index 198cd76..c7e3191 100644 --- a/src/main/java/de/srsoftware/web4rail/moving/Train.java +++ b/src/main/java/de/srsoftware/web4rail/moving/Train.java @@ -979,7 +979,7 @@ public class Train extends BaseClass implements Comparable { autopilot |= auto; if (isSet(nextPreparedRoute)) { LOG.debug("starting nextPreparedRoute: {}",nextPreparedRoute); - if (nextPreparedRoute.start()) { + if (nextPreparedRoute.startNow()) { LOG.debug("dropped nextPreparedRoute (was {})",nextPreparedRoute); nextPreparedRoute = null; return null; diff --git a/src/main/java/de/srsoftware/web4rail/threads/RoutePrepper.java b/src/main/java/de/srsoftware/web4rail/threads/RoutePrepper.java index d63c2f0..87201f4 100644 --- a/src/main/java/de/srsoftware/web4rail/threads/RoutePrepper.java +++ b/src/main/java/de/srsoftware/web4rail/threads/RoutePrepper.java @@ -1,11 +1,11 @@ package de.srsoftware.web4rail.threads; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map.Entry; import java.util.TreeMap; -import java.util.Vector; import de.srsoftware.web4rail.Application; import de.srsoftware.web4rail.BaseClass; @@ -17,6 +17,23 @@ import de.srsoftware.web4rail.tiles.Block; import de.srsoftware.web4rail.tiles.Tile; public class RoutePrepper extends BaseClass implements Runnable{ + + private static class Candidate{ + + private int score; + private Route route; + + public Candidate(Route r, int s) { + route = r; + score = s; + } + + @Override + public String toString() { + return route+"(score: "+score+")"; + } + } + private Context context; private Route route; private List failListeners = new LinkedList<>(); @@ -33,118 +50,113 @@ public class RoutePrepper extends BaseClass implements Runnable{ if (!errors.isEmpty()) throw new NullPointerException(String.join(", ", errors)); context = c; } - - private static TreeMap> availableRoutes(Context context, HashSet visitedRoutes) { - String inset = ""; - for (int i = 0; i < visitedRoutes.size(); i++) inset += " "; - LOG.debug("{}{}.availableRoutes({})", inset, RoutePrepper.class.getSimpleName(), context); - - Block block = context.block(); - Train train = context.train(); - Direction startDirection = context.direction(); - Route currentRoute = context.route(); - TreeMap> availableRoutes = new TreeMap>(); - + + private static TreeMap> availableRoutes(Context c){ boolean error = false; - if (isNull(block) && (error = true)) - LOG.warn("{} → {}.availableRoutes called without context.block!", inset, Train.class.getSimpleName()); - if (isNull(train) && (error = true)) - LOG.warn("{}→ {}.availableRoutes called without context.train!", inset, Train.class.getSimpleName()); - if (error) return availableRoutes; - - if (isSet(startDirection)) { - LOG.debug("{}- Looking for {}-bound routes from {}", inset, startDirection, block); - } else { - LOG.debug("{}- Looking for all routes from {}", inset, block); - } + + Block startBlock = c.block(); + if (isNull(startBlock) && (error=true)) LOG.warn("RoutePrepper.findRoute(…) called without a startBlock!"); + + Train train = c.train(); + if (isNull(train) && (error=true)) LOG.warn("RoutePrepper.findRoute(…) called without a startBlock!"); + + if (error) return new TreeMap<>(); Block destination = train.destination(); - if (isSet(destination) && visitedRoutes.isEmpty()) LOG.debug("{}- Destination: {}", inset, destination); - - for (Route routeCandidate : block.leavingRoutes()) { - if (context.invalidated()) return availableRoutes; - if (visitedRoutes.contains(routeCandidate)) { - LOG.debug("{}→ Candidate {} would create loop, skipping", inset, routeCandidate.shortName()); - continue; - } - - HashSet stuckTrace = train.stuckTrace(); // if train has been stopped in between two blocks lastly: - // only allow starting routes that do not conflict with current train - // position - if (isSet(stuckTrace) && visitedRoutes.isEmpty() && !routeCandidate.path().containsAll(stuckTrace)) { - LOG.debug("Stuck train occupies tiles ({}) outside of {} – not allowed.", stuckTrace, routeCandidate); - continue; - } - - if (!routeCandidate.allowed(context)) { - if (routeCandidate.endBlock() != destination) { // allowance may be overridden by destination - LOG.debug("{} not allowed for {}", routeCandidate, context); - continue; // Zug darf auf Grund einer nicht erfüllten Bedingung nicht auf die Route + + Direction startDirection = c.direction(); + LOG.debug("RoutePrepper.findRoute({},{},{}), dest = {}",startBlock,startDirection,train,destination); + + TreeMap> candidates = routesFrom(c); + + if (isNull(destination)) { + LOG.debug("{} has no destination, returning {}",train,candidates); + return candidates; + } + + LOG.debug("{} is heading for {}, starting breadth-first search…",train,destination); + + HashMap predecessors = new HashMap<>(); + TreeMap> routesToDest = new TreeMap<>(); + + int level = 0; + + while (!candidates.isEmpty()) { + TreeMap> queue = new TreeMap<>(); + + while (!candidates.isEmpty()) { + Candidate candidate = pop(candidates); + LOG.debug(" - examining {}…",candidate); + + Block endBlock = candidate.route.endBlock(); + Direction endDir = candidate.route.endDirection; + + if (endBlock == destination) { + LOG.debug(" - {} reaches destination!",candidate); + int score = candidate.score; + + // The route we found leads to the destination block. + // However it might be the last route in a long path. + // Thus, we need to get the first route in this path: + while (predecessors.containsKey(candidate.route)) { + candidate = predecessors.get(candidate.route); + LOG.debug(" - predecessed by {}",candidate); + score += candidate.score; + } + + LOG.debug(" → path starts with {} and has total score of {}",candidate.route,score); + LinkedList routesForScore = routesToDest.get(score); + if (isNull(routesForScore)) routesToDest.put(score, routesForScore = new LinkedList()); + routesForScore.add(candidate.route); + continue; } - LOG.debug("{} not allowed for {} – overridden by selected destination", routeCandidate, context); - } - - int priority = 0; - if (isSet(startDirection) && routeCandidate.startDirection != startDirection) { // Route startet entgegen - // der aktuellen - // Fahrtrichtung des Zuges - if (!train.pushPull) continue; // Zug kann nicht wenden - if (!block.turnAllowed) continue; // Wenden im Block nicht gestattet - priority -= 5; - } - if (routeCandidate == currentRoute) priority -= 10; // möglichst andere Route als zuvor wählen // TODO: den - // Routen einen "last-used" Zeitstempel hinzufügen, und - // diesen mit in die Priorisierung einbeziehen - - if (isSet(destination)) { - if (routeCandidate.endBlock() == destination) { // route goes directly to destination - LOG.debug("{}→ Candidate {} directly leads to {}", inset, routeCandidate.shortName(), destination); - priority = 1_000_000; - } else { - LOG.debug("{}- Candidate: {}", inset, routeCandidate.shortName()); - Context forwardContext = new Context(train).block(routeCandidate.endBlock()).route(null) - .direction(routeCandidate.endDirection); - visitedRoutes.add(routeCandidate); - TreeMap> forwardRoutes = availableRoutes(forwardContext, visitedRoutes); - visitedRoutes.remove(routeCandidate); - if (forwardRoutes.isEmpty()) continue; // the candidate does not lead to a block, from which routes - // to the destination exist - Entry> entry = forwardRoutes.lastEntry(); - LOG.debug("{}→ The following routes have connections to {}:", inset, destination); - for (Route rt : entry.getValue()) LOG.debug("{} - {}", inset, rt.shortName()); - priority += entry.getKey() - 10; + + LOG.debug(" - {} not reaching {}, adding ongoing routes to queue:",candidate,destination); + TreeMap> successors = routesFrom(c.clone().block(endBlock).direction(endDir)); + while (!successors.isEmpty()) { + int score = successors.firstKey(); + LinkedList best = successors.remove(score); + score -= 25; // Nachfolgeroute + for (Route route : best) { + LOG.debug(" - queueing {} with score {}",route,score); + if (predecessors.containsKey(route)) continue; // Route wurde bereits besucht + predecessors.put(route, candidate); + + LinkedList list = queue.get(score); + if (isNull(list)) queue.put(score, list = new LinkedList<>()); + list.add(route); + } } } - - List routeSet = availableRoutes.get(priority); - if (isNull(routeSet)) { - routeSet = new Vector(); - availableRoutes.put(priority, routeSet); - } - routeSet.add(routeCandidate); - if (routeCandidate.endBlock() == destination) break; // direct connection to destination discovered, quit - // search - } - if (!availableRoutes.isEmpty()) - LOG.debug("{}→ Routes from {}: {}", inset, block, availableRoutes.isEmpty() ? "none" : ""); - for (Entry> entry : availableRoutes.entrySet()) { - LOG.debug("{} - Priority {}:", inset, entry.getKey()); - for (Route r : entry.getValue()) LOG.debug("{} - {}", inset, r.shortName()); + + if (!routesToDest.isEmpty()) return routesToDest; + LOG.debug("No routes to {} found with distance {}!",destination,level); + level ++; + candidates = queue; } - return availableRoutes; + LOG.debug("No more candidates for routes towards {}!",destination); + + return new TreeMap<>(); + } private static Route chooseRoute(Context context) { LOG.debug("{}.chooseRoute({})", RoutePrepper.class.getSimpleName(), context); - TreeMap> availableRoutes = availableRoutes(context, new HashSet()); + TreeMap> availableRoutes = availableRoutes(context); + LOG.debug("available routes: {}",availableRoutes); while (!availableRoutes.isEmpty()) { if (context.invalidated()) break; LOG.debug("availableRoutes: {}", availableRoutes); - Entry> entry = availableRoutes.lastEntry(); + Entry> entry = availableRoutes.lastEntry(); List preferredRoutes = entry.getValue(); LOG.debug("preferredRoutes: {}", preferredRoutes); Route selectedRoute = preferredRoutes.get(random.nextInt(preferredRoutes.size())); - if (selectedRoute.isFreeFor(context)) { + + HashSet stuckTrace = context.train().stuckTrace(); // if train has been stopped in between two blocks lastly: + // only allow starting routes that do not conflict with current train position + if (isSet(stuckTrace) && !selectedRoute.path().containsAll(stuckTrace)) { + LOG.debug("Stuck train occupies tiles ({}) outside of {} – not allowed.", stuckTrace, selectedRoute); + } else if (selectedRoute.isFreeFor(context)) { LOG.debug("Chose \"{}\" with priority {}.", selectedRoute, entry.getKey()); return selectedRoute; } @@ -156,6 +168,11 @@ public class RoutePrepper extends BaseClass implements Runnable{ return null; } + private boolean fail() { + notify(failListeners); + route = null; + return false; + } private void notify(List listeners) { for (EventListener listener: listeners) { @@ -163,13 +180,6 @@ public class RoutePrepper extends BaseClass implements Runnable{ } } - private boolean fail() { - notify(failListeners); - // if (isSet(route) route.reset(); - route = null; - return false; - } - public void onFail(EventListener l) { failListeners.add(l); } @@ -186,7 +196,20 @@ public class RoutePrepper extends BaseClass implements Runnable{ preparedListener = l; } - + private static Candidate pop(TreeMap> candidates) { + while (!candidates.isEmpty()) { + int score = candidates.firstKey(); + LinkedList list = candidates.get(score); + if (isNull(list) || list.isEmpty()) { + candidates.remove(score); + } else { + Candidate candidate = new Candidate(list.removeFirst(),score); + if (list.isEmpty()) candidates.remove(score); + return candidate; + } + } + return null; + } public boolean prepareRoute() { if (isNull(context) || context.invalidated()) return fail(); @@ -206,6 +229,53 @@ public class RoutePrepper extends BaseClass implements Runnable{ return route; } + private static TreeMap> routesFrom(Context c){ + boolean error = false; + + Block startBlock = c.block(); + if (isNull(startBlock) && (error=true)) LOG.warn("RoutePrepper.routesFrom(…) called without a startBlock!"); + + Train train = c.train(); + if (isNull(train) && (error=true)) LOG.warn("RoutePrepper.routesFrom(…) called without a startBlock!"); + + if (error) return null; + + Block destination = train.destination(); + + Direction startDirection = c.direction(); + + LOG.debug("RoutePrepper.routesFrom({},{},{}), dest = {}",startBlock,startDirection,train,destination); + + TreeMap> routes = new TreeMap<>(); + + for (Route route : startBlock.leavingRoutes()) { + LOG.debug(" - evaluating {}",route); + + int score = 0; + + if (!route.allowed(new Context(train).block(startBlock).direction(startDirection))) { + LOG.debug(" - {} not allowed for {}", route, train); + if (route.endBlock() != destination) continue; + LOG.debug(" …overridden by destination of train!", route, train); + } + + if (route.endBlock() == destination) score = 100_000; + + if (isSet(startDirection) && route.startDirection != startDirection) { // Route startet entgegen der aktuellen Fahrtrichtung des Zuges + if (!train.pushPull) continue; // Zug kann nicht wenden + if (!startBlock.turnAllowed) continue; // Wenden im Block nicht gestattet + score -= 5; + } + + LinkedList routesForScore = routes.get(score); + if (isNull(routesForScore)) routes.put(score, routesForScore = new LinkedList()); + LOG.debug(" → candidate!"); + routesForScore.add(route); + } + + return routes; + } + @Override public void run() { LOG.debug("{}.run()",this); diff --git a/src/main/java/de/srsoftware/web4rail/tiles/Contact.java b/src/main/java/de/srsoftware/web4rail/tiles/Contact.java index 3ac8baa..0a62aa5 100644 --- a/src/main/java/de/srsoftware/web4rail/tiles/Contact.java +++ b/src/main/java/de/srsoftware/web4rail/tiles/Contact.java @@ -27,7 +27,6 @@ import de.srsoftware.web4rail.tags.Window; import de.srsoftware.web4rail.threads.DelayedExecution; public class Contact extends Tile{ - private static Logger LOG = LoggerFactory.getLogger(Contact.class); private static final String ADDRESS = "address"; private static final HashMap contactsByAddr = new HashMap(); private boolean state = false; diff --git a/src/main/java/de/srsoftware/web4rail/tiles/Turnout.java b/src/main/java/de/srsoftware/web4rail/tiles/Turnout.java index cdb75d1..a1dc98f 100644 --- a/src/main/java/de/srsoftware/web4rail/tiles/Turnout.java +++ b/src/main/java/de/srsoftware/web4rail/tiles/Turnout.java @@ -52,7 +52,7 @@ public abstract class Turnout extends Tile implements Device{ @Override public Object click(boolean shift) throws IOException { LOG.debug(getClass().getSimpleName()+".click()"); - if (!shift) init(); + init(); return super.click(shift); } @@ -159,9 +159,13 @@ public abstract class Turnout extends Tile implements Device{ return state; } - public Reply state(State newState) { + public Reply state(State newState,boolean shift) { Train lockingTrain = lockingTrain(); - if (isSet(lockingTrain) && newState != state) return new Reply(415, t("{} locked by {}!",this,lockingTrain)); + + if (isSet(lockingTrain)) { + if (newState != state && !shift) return new Reply(415, t("{} locked by {}!",this,lockingTrain)); + // shift allows to switch locked turnouts... + } else if (shift) return new Reply(200,"OK"); // shift on a non-locked turnout skips the switch process if (address == 0) { sleep(300); state = newState; diff --git a/src/main/java/de/srsoftware/web4rail/tiles/TurnoutL.java b/src/main/java/de/srsoftware/web4rail/tiles/TurnoutL.java index 2779e33..62917ea 100644 --- a/src/main/java/de/srsoftware/web4rail/tiles/TurnoutL.java +++ b/src/main/java/de/srsoftware/web4rail/tiles/TurnoutL.java @@ -15,7 +15,7 @@ public abstract class TurnoutL extends Turnout { @Override public Object click(boolean shift) throws IOException { Object o = super.click(shift); - if (!shift) state(state == State.STRAIGHT ? State.LEFT : State.STRAIGHT); + state(state == State.STRAIGHT ? State.LEFT : State.STRAIGHT,shift); return o; } diff --git a/src/main/java/de/srsoftware/web4rail/tiles/TurnoutR.java b/src/main/java/de/srsoftware/web4rail/tiles/TurnoutR.java index f639c71..20bdec0 100644 --- a/src/main/java/de/srsoftware/web4rail/tiles/TurnoutR.java +++ b/src/main/java/de/srsoftware/web4rail/tiles/TurnoutR.java @@ -15,13 +15,12 @@ public abstract class TurnoutR extends Turnout { @Override public Object click(boolean shift) throws IOException { Object o = super.click(shift); - if (!shift) state(state == State.STRAIGHT ? State.RIGHT : State.STRAIGHT); + state(state == State.STRAIGHT ? State.RIGHT : State.STRAIGHT,shift); return o; } @Override public String commandFor(State newState) { - switch (newState) { case RIGHT: return "SET {} GA "+address+" "+portB+" 1 "+delay;