/* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (props, at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.opentripplanner.routing.core; import java.util.Arrays; import java.util.HashMap; import java.util.Set; import java.util.HashSet; import java.util.List; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.gtfs.model.Stop; import org.onebusaway.gtfs.model.Trip; import org.opentripplanner.routing.automata.AutomatonState; import org.opentripplanner.routing.edgetype.TripPattern; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.patch.Alert; import org.opentripplanner.routing.patch.Patch; import org.opentripplanner.routing.pathparser.PathParser; import org.opentripplanner.routing.trippattern.TripTimes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class is a wrapper around a new State that provides it with setter and increment methods, * allowing it to be modified before being put to use. * * By virtue of being in the same package as States, it can modify their package private fields. * * @author andrewbyrd */ public class StateEditor { private static final Logger LOG = LoggerFactory.getLogger(StateEditor.class); protected State child; private boolean extensionsModified = false; private boolean spawned = false; private boolean defectiveTraversal = false; private boolean traversingBackward; // we use our own set of notes and only replace the child notes if they're different private Set<Alert> notes = null; /* CONSTRUCTORS */ protected StateEditor() {} public StateEditor(RoutingRequest options, Vertex v) { child = new State(v, options); child.stateData = new StateData(options); } public StateEditor(State parent, Edge e) { child = parent.clone(); child.backState = parent; child.backEdge = e; // We clear child.next here, since it could have already been set in the // parent child.next = null; if (e == null) { child.backState = null; child.vertex = parent.vertex; child.stateData = child.stateData.clone(); } else { // be clever // Note that we use equals(), not ==, here to allow for dynamically // created vertices if (e.getFromVertex().equals(e.getToVertex()) && parent.vertex.equals(e.getFromVertex())) { // TODO LG: We disable this test: the assumption that // the from and to vertex of an edge are not the same // is not true anymore: bike rental on/off edges. traversingBackward = parent.getOptions().isArriveBy(); child.vertex = e.getToVertex(); } else if (parent.vertex.equals(e.getFromVertex())) { traversingBackward = false; child.vertex = e.getToVertex(); } else if (parent.vertex.equals(e.getToVertex())) { traversingBackward = true; child.vertex = e.getFromVertex(); } else { // Parent state is not at either end of edge. LOG.warn("Edge is not connected to parent state: {}", e); LOG.warn(" from vertex: {}", e.getFromVertex()); LOG.warn(" to vertex: {}", e.getToVertex()); LOG.warn(" parent vertex: {}", parent.vertex); defectiveTraversal = true; } if (traversingBackward != parent.getOptions().isArriveBy()) { LOG.error("Actual traversal direction does not match traversal direction in TraverseOptions."); defectiveTraversal = true; } } } /* PUBLIC METHODS */ /** * Why can a state editor only be used once? If you modify some component of state with and * editor, use the editor to create a new state, and then make more modifications, these * modifications will be applied to the previously created state. Reusing the state editor to * make several states would modify an existing state somewhere earlier in the search, messing * up the shortest path tree. */ public State makeState() { // check that this editor has not been used already if (spawned) throw new IllegalStateException("A StateEditor can only be used once."); // if something was flagged incorrect, do not make a new state if (defectiveTraversal) { LOG.error("Defective traversal flagged on edge " + child.backEdge); return null; } if (child.backState != null) { // make it impossible to use a state with lower weight than its // parent. child.checkNegativeWeight(); // check that time changes are coherent with edge traversal // direction if (traversingBackward ? (child.getTimeDeltaSeconds() > 0) : (child.getTimeDeltaSeconds() < 0)) { LOG.trace("Time was incremented the wrong direction during state editing. {}", child.backEdge); return null; } if(!applyPatches()) { return null; } } if ( ! parsePath(this.child)) return null; // copy the notes if need be, keeping in mind they may both be null if (this.notes != child.stateData.notes) { cloneStateDataAsNeeded(); child.stateData.notes = this.notes; } spawned = true; return child; } public boolean weHaveWalkedTooFar(RoutingRequest options) { // Only apply limit in transit-only case if (!options.getModes().isTransit()) return false; return child.walkDistance >= options.maxWalkDistance; } public String toString() { return "<StateEditor " + child + ">"; } /* PUBLIC METHODS TO MODIFY A STATE BEFORE IT IS USED */ /** * Put a new value into the State extensions map. This will always clone the extensions map * before it is modified the first time, making sure that other references to the map in earlier * States are unaffected. */ @SuppressWarnings("unchecked") public void setExtension(Object key, Object value) { cloneStateDataAsNeeded(); if (!extensionsModified) { HashMap<Object, Object> newExtensions; if (child.stateData.extensions == null) newExtensions = new HashMap<Object, Object>(4); else newExtensions = (HashMap<Object, Object>) child.stateData.extensions.clone(); child.stateData.extensions = newExtensions; extensionsModified = true; } child.stateData.extensions.put(key, value); } /** * Tell the stateEditor to return null when makeState() is called, no matter what other editing * has been done. This allows graph patches to block traversals. */ public void blockTraversal() { this.defectiveTraversal = true; } /** * Add an alert to this state. This used to use an EdgeNarrative */ public void addAlert(Alert notes) { if (notes == null) return; if (this.notes == null) this.notes = new HashSet<Alert>(); this.notes.add(notes); } /** * Convenience function to add multiple alerts */ public void addAlerts(Iterable<Alert> alerts) { if (alerts == null) return; for (Alert alert : alerts) { this.addAlert(alert); } } /* Incrementors */ public void incrementWeight(double weight) { if (Double.isNaN(weight)) { LOG.warn("A state's weight is being incremented by NaN while traversing edge " + child.backEdge); defectiveTraversal = true; return; } if (weight < 0) { LOG.warn("A state's weight is being incremented by a negative amount while traversing edge " + child.backEdge); defectiveTraversal = true; return; } child.weight += weight; } /** * Advance or rewind the time of the new state by the given non-negative amount. Direction of * time is inferred from the direction of traversal. This is the only element of state that runs * backward when traversing backward. */ public void incrementTimeInSeconds(int seconds) { incrementTimeInMilliseconds(seconds * 1000); } public void incrementTimeInMilliseconds(int milliseconds) { if (milliseconds < 0) { LOG.warn("A state's time is being incremented by a negative amount while traversing edge " + child.getBackEdge()); defectiveTraversal = true; return; } child.time += (traversingBackward ? -milliseconds : milliseconds); } public void incrementWalkDistance(double length) { if (length < 0) { LOG.warn("A state's walk distance is being incremented by a negative amount."); defectiveTraversal = true; return; } child.walkDistance += length; } public void incrementNumBoardings() { cloneStateDataAsNeeded(); child.stateData.numBoardings++; } /* Basic Setters */ public void setTripTimes(TripTimes tripTimes) { cloneStateDataAsNeeded(); child.stateData.tripTimes = tripTimes; } public void setTripId(AgencyAndId tripId) { cloneStateDataAsNeeded(); child.stateData.tripId = tripId; } public void setPreviousTrip(Trip previousTrip) { cloneStateDataAsNeeded(); child.stateData.previousTrip = previousTrip; } public void setInitialWaitTimeSeconds(long initialWaitTimeSeconds) { cloneStateDataAsNeeded(); child.stateData.initialWaitTime = initialWaitTimeSeconds; } public void setBackMode(TraverseMode mode) { if (mode == child.stateData.backMode) return; cloneStateDataAsNeeded(); child.stateData.backMode = mode; } public void setBackWalkingBike (boolean walkingBike) { if (walkingBike == child.stateData.backWalkingBike) return; cloneStateDataAsNeeded(); child.stateData.backWalkingBike = walkingBike; } /** * The lastNextArrivalDelta is the amount of time between the arrival of the last trip * the planner used and the arrival of the trip after that. */ public void setLastNextArrivalDelta (int lastNextArrivalDelta) { cloneStateDataAsNeeded(); child.stateData.lastNextArrivalDelta = lastNextArrivalDelta; } public void setWalkDistance(double walkDistance) { child.walkDistance = walkDistance; } public void setZone(String zone) { if (zone == null) { if (child.stateData.zone != null) { cloneStateDataAsNeeded(); child.stateData.zone = zone; } } else if (!zone.equals(child.stateData.zone)) { cloneStateDataAsNeeded(); child.stateData.zone = zone; } } public void setRoute(AgencyAndId routeId) { cloneStateDataAsNeeded(); child.stateData.route = routeId; // unlike tripId, routeId is not set to null when alighting // but do a null check anyway if (routeId != null) { AgencyAndId[] oldRouteSequence = child.stateData.routeSequence; //LOG.debug("old route seq {}", Arrays.asList(oldRouteSequence)); int oldLength = oldRouteSequence.length; child.stateData.routeSequence = Arrays.copyOf(oldRouteSequence, oldLength + 1); child.stateData.routeSequence[oldLength] = routeId; //LOG.debug("new route seq {}", Arrays.asList(child.stateData.routeSequence)); // array will be interpreted as varargs } } public void setNumBoardings(int numBoardings) { cloneStateDataAsNeeded(); child.stateData.numBoardings = numBoardings; } public void setAlightedLocal(boolean alightedLocal) { cloneStateDataAsNeeded(); child.stateData.alightedLocal = alightedLocal; } public void setEverBoarded(boolean everBoarded) { cloneStateDataAsNeeded(); child.stateData.everBoarded = everBoarded; } public void setBikeRenting(boolean bikeRenting) { cloneStateDataAsNeeded(); child.stateData.usingRentedBike = bikeRenting; if (bikeRenting) { child.stateData.nonTransitMode = TraverseMode.BICYCLE; } else { child.stateData.nonTransitMode = TraverseMode.WALK; } } public void setPreviousStop(Stop previousStop) { cloneStateDataAsNeeded(); child.stateData.previousStop = previousStop; } public void setLastAlightedTimeSeconds(long lastAlightedTimeSeconds) { cloneStateDataAsNeeded(); child.stateData.lastAlightedTime = lastAlightedTimeSeconds; } public void setTimeSeconds(long seconds) { child.time = seconds * 1000; } public void setStartTimeSeconds(long seconds) { cloneStateDataAsNeeded(); child.stateData.startTime = seconds; } /** * Set non-incremental state values (ex. {@link State#getRoute()}) from an existing state. * Incremental values (ex. {@link State#getNumBoardings()}) are not currently set. * * @param state */ public void setFromState(State state) { cloneStateDataAsNeeded(); child.stateData.route = state.stateData.route; child.stateData.tripTimes = state.stateData.tripTimes; child.stateData.tripId = state.stateData.tripId; child.stateData.previousTrip = state.stateData.previousTrip; child.stateData.previousStop = state.stateData.previousStop; child.stateData.zone = state.stateData.zone; child.stateData.extensions = state.stateData.extensions; child.stateData.usingRentedBike = state.stateData.usingRentedBike; } /* PUBLIC GETTER METHODS */ /* * Allow patches to see the State being edited, so they can decide whether to apply their * transformations to the traversal result or not. */ public Object getExtension(Object key) { return child.getExtension(key); } public long getTimeSeconds() { return child.getTimeSeconds(); } public long getElapsedTimeSeconds() { return child.getElapsedTimeSeconds(); } public AgencyAndId getTripId() { return child.getTripId(); } public Trip getPreviousTrip() { return child.getPreviousTrip(); } public String getZone() { return child.getZone(); } public AgencyAndId getRoute() { return child.getRoute(); } public int getNumBoardings() { return child.getNumBoardings(); } public boolean isAlightedLocal() { return child.isAlightedLocal(); } public boolean isEverBoarded() { return child.isEverBoarded(); } public boolean isRentingBike() { return child.isBikeRenting(); } public long getLastAlightedTimeSeconds() { return child.getLastAlightedTimeSeconds(); } public double getWalkDistance() { return child.getWalkDistance(); } public Vertex getVertex() { return child.getVertex(); } /* PRIVATE METHODS */ /** * Find any patches that have been applied to the edge being traversed (i.e. the new child * state's back edge) and allow these patches to manipulate the StateEditor before the child * state is put to use. * * @return false if a patch blocked traversal */ private boolean applyPatches() { List<Patch> patches = child.backEdge.getPatches(); boolean display = false, active = false; if (patches != null) { for (Patch patch : patches) { active = false; display = patch.displayDuring(child.stateData.opt, child.getStartTimeSeconds(), child.getTimeSeconds()); if(!display) { active = patch.activeDuring(child.stateData.opt, child.getStartTimeSeconds(), child.getTimeSeconds()); } if(display || active) { if(!patch.filterTraverseResult(this, display)) return false; } } } return true; } /** * To be called before modifying anything in the child's StateData. Makes sure that changes are * applied to a copy of StateData rather than the same one that is still referenced in existing, * older states. */ private void cloneStateDataAsNeeded() { if (child.backState != null && child.stateData == child.backState.stateData) child.stateData = child.stateData.clone(); } /** return true if all PathParsers advanced to a state other than REJECT */ public boolean parsePath(State state) { if (state.stateData.opt.rctx == null) return true; // a lot of tests don't set a routing context PathParser[] parsers = state.stateData.opt.rctx.pathParsers; int[] parserStates = state.pathParserStates; boolean accept = true; boolean modified = false; int i = 0; for (PathParser parser : parsers) { int terminal = parser.terminalFor(state); int oldState = parserStates[i]; int newState = parser.transition(oldState, terminal); if (newState != oldState) { if (!modified) { // clone the state array so only the new state will see modifications parserStates = parserStates.clone(); modified = true; } parserStates[i] = newState; if (newState == AutomatonState.REJECT) accept = false; } i++; } if (modified) state.pathParserStates = parserStates; return accept; } public void alightTransit() { cloneStateDataAsNeeded(); child.stateData.lastTransitWalk = child.getWalkDistance(); } public void setLastPattern(TripPattern pattern) { cloneStateDataAsNeeded(); child.stateData.lastPattern = pattern; } public void setOptions(RoutingRequest options) { cloneStateDataAsNeeded(); child.stateData.opt = options; } public void setServiceDay(ServiceDay day) { cloneStateDataAsNeeded(); child.stateData.serviceDay = day; } public void setBikeRentalNetwork(Set<String> networks) { cloneStateDataAsNeeded(); child.stateData.bikeRentalNetworks = networks; } }