/* 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 (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.edgetype; import java.io.IOException; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import lombok.Getter; import lombok.Setter; import org.opentripplanner.common.TurnRestriction; import org.opentripplanner.common.TurnRestrictionType; import org.opentripplanner.common.geometry.DirectionUtils; import org.opentripplanner.common.geometry.PackedCoordinateSequence; import org.opentripplanner.routing.core.RoutingRequest; import org.opentripplanner.routing.core.State; import org.opentripplanner.routing.core.StateEditor; import org.opentripplanner.routing.core.TraverseMode; import org.opentripplanner.routing.core.TraverseModeSet; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.patch.Alert; import org.opentripplanner.routing.util.ElevationProfileSegment; import org.opentripplanner.routing.util.ElevationUtils; import org.opentripplanner.routing.vertextype.IntersectionVertex; import org.opentripplanner.routing.vertextype.StreetVertex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.LineString; /** * This represents a street segment. * * @author novalis * */ public class PlainStreetEdge extends StreetEdge implements Cloneable { private static Logger LOG = LoggerFactory.getLogger(PlainStreetEdge.class); private static final long serialVersionUID = 1L; private static final double GREENWAY_SAFETY_FACTOR = 0.1; private ElevationProfileSegment elevationProfileSegment; @Getter private double length; @Getter private LineString geometry; @Getter @Setter private String name; @Getter @Setter private String label; @Getter @Setter private boolean wheelchairAccessible = true; @Getter @Setter private StreetTraversalPermission permission; @Getter @Setter private int streetClass = CLASS_OTHERPATH; /** * Marks that this edge is the reverse of the one defined in the source * data. Does NOT mean fromv/tov are reversed. */ @Getter @Setter public boolean back; @Getter @Setter private boolean roundabout = false; @Getter private Set<Alert> notes; @Setter private boolean hasBogusName; @Getter @Setter private boolean noThruTraffic; /** * This street is a staircase */ @Getter @Setter private boolean stairs; /** * The speed (meters / sec) at which an automobile can traverse * this street segment. */ @Getter @Setter private float carSpeed; /** This street has a toll */ @Getter @Setter private boolean toll; @Getter private Set<Alert> wheelchairNotes; @Getter private List<TurnRestriction> turnRestrictions = Collections.emptyList(); /** 0 -> 360 degree angle - the angle at the start of the edge geometry */ @Getter public int inAngle; /** 0 -> 360 degree angle - the angle at the end of the edge geometry */ @Getter public int outAngle; /** * No-arg constructor used only for customization -- do not call this unless you know * what you are doing */ public PlainStreetEdge() { super(null, null); } public PlainStreetEdge(StreetVertex v1, StreetVertex v2, LineString geometry, String name, double length, StreetTraversalPermission permission, boolean back) { // use a default car speed of ~25 mph for splitter vertices and the like // TODO(flamholz): do something smarter with the car speed here. this(v1, v2, geometry, name, length, permission, back, 11.2f); } public PlainStreetEdge(StreetVertex v1, StreetVertex v2, LineString geometry, String name, double length, StreetTraversalPermission permission, boolean back, float carSpeed) { super(v1, v2); this.geometry = geometry; this.length = length; this.elevationProfileSegment = new ElevationProfileSegment(length); this.name = name; this.permission = permission; this.back = back; this.carSpeed = carSpeed; if (geometry != null) { try { for (Coordinate c : geometry.getCoordinates()) { if (Double.isNaN(c.x)) { System.out.println("X DOOM"); } if (Double.isNaN(c.y)) { System.out.println("Y DOOM"); } } double angleR = DirectionUtils.getLastAngle(geometry); outAngle = ((int) Math.toDegrees(angleR) + 180) % 360; angleR = DirectionUtils.getFirstAngle(geometry); inAngle = ((int) Math.toDegrees(angleR) + 180) % 360; } catch (IllegalArgumentException iae) { LOG.error("exception while determining street edge angles. setting to zero. there is probably something wrong with this street segment's geometry."); inAngle = 0; outAngle = 0; } } } @Override public boolean canTraverse(RoutingRequest options) { if (options.wheelchairAccessible) { if (!wheelchairAccessible) { return false; } if (elevationProfileSegment.getMaxSlope() > options.maxSlope) { return false; } } return canTraverse(options.getModes()); } @Override public boolean canTraverse(TraverseModeSet modes) { return permission.allows(modes); } private boolean canTraverse(RoutingRequest options, TraverseMode mode) { if (options.wheelchairAccessible) { if (!wheelchairAccessible) { return false; } if (elevationProfileSegment.getMaxSlope() > options.maxSlope) { return false; } } return permission.allows(mode); } @Override public PackedCoordinateSequence getElevationProfile() { return elevationProfileSegment.getElevationProfile(); } @Override public boolean setElevationProfile(PackedCoordinateSequence elev, boolean computed) { return elevationProfileSegment.setElevationProfile(elev, computed, permission.allows(StreetTraversalPermission.CAR)); } @Override public boolean isElevationFlattened() { return elevationProfileSegment.isFlattened(); } @Override public double getDistance() { return length; } @Override public State traverse(State s0) { final RoutingRequest options = s0.getOptions(); return doTraverse(s0, options, s0.getNonTransitMode()); } private State doTraverse(State s0, RoutingRequest options, TraverseMode traverseMode) { boolean walkingBike = options.isWalkingBike(); boolean backWalkingBike = s0.isBackWalkingBike(); TraverseMode backMode = s0.getBackMode(); Edge backEdge = s0.getBackEdge(); if (backEdge != null) { // No illegal U-turns. // NOTE(flamholz): we check both directions because both edges get a chance to decide // if they are the reverse of the other. Also, because it doesn't matter which direction // we are searching in - these traversals are always disallowed (they are U-turns in one direction // or the other). if (this.isReverseOf(backEdge) || backEdge.isReverseOf(this)) { return null; } } // Ensure we are actually walking, when walking a bike backWalkingBike &= TraverseMode.WALK.equals(backMode); walkingBike &= TraverseMode.WALK.equals(traverseMode); if (!canTraverse(options, traverseMode)) { if (traverseMode == TraverseMode.BICYCLE) { // try walking bike since you can't ride here return doTraverse(s0, options.getBikeWalkingOptions(), TraverseMode.WALK); } return null; } // Automobiles have variable speeds depending on the edge type double speed = calculateSpeed(options, traverseMode); double time = length / speed; double weight; // TODO(flamholz): factor out this bike, wheelchair and walking specific logic to somewhere central. if (options.wheelchairAccessible) { weight = elevationProfileSegment.getSlopeSpeedEffectiveLength() / speed; } else if (traverseMode.equals(TraverseMode.BICYCLE)) { time = elevationProfileSegment.getSlopeSpeedEffectiveLength() / speed; switch (options.optimize) { case SAFE: weight = elevationProfileSegment.getBicycleSafetyEffectiveLength() / speed; break; case GREENWAYS: weight = elevationProfileSegment.getBicycleSafetyEffectiveLength() / speed; if (elevationProfileSegment.getBicycleSafetyEffectiveLength() / length <= GREENWAY_SAFETY_FACTOR) { // greenways are treated as even safer than they really are weight *= 0.66; } break; case FLAT: /* see notes in StreetVertex on speed overhead */ weight = length / speed + elevationProfileSegment.getSlopeWorkCost(); break; case QUICK: weight = elevationProfileSegment.getSlopeSpeedEffectiveLength() / speed; break; case TRIANGLE: double quick = elevationProfileSegment.getSlopeSpeedEffectiveLength(); double safety = elevationProfileSegment.getBicycleSafetyEffectiveLength(); double slope = elevationProfileSegment.getSlopeWorkCost(); weight = quick * options.getTriangleTimeFactor() + slope * options.getTriangleSlopeFactor() + safety * options.getTriangleSafetyFactor(); weight /= speed; break; default: weight = length / speed; } } else { if (walkingBike) { // take slopes into account when walking bikes time = elevationProfileSegment.getSlopeSpeedEffectiveLength() / speed; } weight = time; if (traverseMode.equals(TraverseMode.WALK)) { // take slopes into account when walking double costs = ElevationUtils.getWalkCostsForSlope(length, elevationProfileSegment.getMaxSlope()); // as the cost walkspeed is assumed to be for 4.8km/h (= 1.333 m/sec) we need to adjust // for the walkspeed set by the user double elevationUtilsSpeed = 4.0 / 3.0; weight = costs * (elevationUtilsSpeed / speed); time = weight; //treat cost as time, as in the current model it actually is the same (this can be checked for maxSlope == 0) /* // debug code if(weight > 100){ double timeflat = length / speed; System.out.format("line length: %.1f m, slope: %.3f ---> slope costs: %.1f , weight: %.1f , time (flat): %.1f %n", length, elevationProfileSegment.getMaxSlope(), costs, weight, timeflat); } */ } } if (isStairs()) { weight *= options.stairsReluctance; } else { weight *= options.walkReluctance; } StateEditor s1 = s0.edit(this); s1.setBackMode(traverseMode); s1.setBackWalkingBike(walkingBike); if (wheelchairNotes != null && options.wheelchairAccessible) { s1.addAlerts(wheelchairNotes); } PlainStreetEdge backPSE; if (backEdge != null && backEdge instanceof PlainStreetEdge) { backPSE = (PlainStreetEdge) backEdge; RoutingRequest backOptions = backWalkingBike ? options.bikeWalkingOptions : options; double backSpeed = backPSE.calculateSpeed(backOptions, backMode); final double realTurnCost; // Units are seconds. /* Compute turn cost. * * This is a subtle piece of code. Turn costs are evaluated differently during * forward and reverse traversal. During forward traversal of an edge, the turn * *into* that edge is used, while during reverse traversal, the turn *out of* * the edge is used. * * However, over a set of edges, the turn costs must add up the same (for * general correctness and specifically for reverse optimization). This means * that during reverse traversal, we must also use the speed for the mode of * the backEdge, rather than of the current edge. */ if (options.arriveBy && tov instanceof IntersectionVertex) { // arrive-by search if (!canTurnOnto(backPSE, s0, backMode)) { return null; } IntersectionVertex traversedVertex = ((IntersectionVertex) tov); realTurnCost = backOptions.getIntersectionTraversalCostModel().computeTraversalCost( traversedVertex, this, backPSE, backMode, backOptions, (float) speed, (float) backSpeed); } else if (fromv instanceof IntersectionVertex) { // depart-after search if (!backPSE.canTurnOnto(this, s0, traverseMode)) { return null; } IntersectionVertex traversedVertex = ((IntersectionVertex) fromv); realTurnCost = options.getIntersectionTraversalCostModel().computeTraversalCost( traversedVertex, backPSE, this, traverseMode, options, (float) backSpeed, (float) speed); } else { // In case this is a temporary edge not connected to an IntersectionVertex LOG.debug("Not computing turn cost for edge {}", this); realTurnCost = 0; } if (!traverseMode.isDriving()) { s1.incrementWalkDistance(realTurnCost / 100); // just a tie-breaker } long turnTime = (long) Math.ceil(realTurnCost); time += turnTime; weight += options.turnReluctance * realTurnCost; } int timeLong = (int) Math.ceil(time); s1.incrementTimeInSeconds(timeLong); s1.incrementWeight(weight); if (!traverseMode.isDriving()) { s1.incrementWalkDistance(length); } if (s1.weHaveWalkedTooFar(options)) { LOG.debug("Too much walking. Bailing."); return null; } s1.addAlerts(notes); if (this.isToll() && traverseMode.isDriving()) { s1.addAlert(Alert.createSimpleAlerts("Toll road")); } return s1.makeState(); } /** * Calculate the average automobile traversal speed of this segment, given * the RoutingRequest, and return it in meters per second. */ private double calculateCarSpeed(RoutingRequest options) { return carSpeed; } /** * Calculate the speed appropriately given the RoutingRequest and traverseMode. */ private double calculateSpeed(RoutingRequest options, TraverseMode traverseMode) { if (traverseMode == null) { return Double.NaN; } else if (traverseMode.isDriving()) { // NOTE: Automobiles have variable speeds depending on the edge type return calculateCarSpeed(options); } return options.getSpeed(traverseMode); } @Override public double weightLowerBound(RoutingRequest options) { return timeLowerBound(options) * options.walkReluctance; } @Override public double timeLowerBound(RoutingRequest options) { return this.length / options.getStreetSpeedUpperBound(); } public void setSlopeSpeedEffectiveLength(double slopeSpeedEffectiveLength) { elevationProfileSegment.setSlopeSpeedEffectiveLength(slopeSpeedEffectiveLength); } public double getSlopeSpeedEffectiveLength() { return elevationProfileSegment.getSlopeSpeedEffectiveLength(); } public void setSlopeWorkCost(double slopeWorkCost) { elevationProfileSegment.setSlopeWorkCost(slopeWorkCost); } public double getWorkCost() { return elevationProfileSegment.getSlopeWorkCost(); } public void setBicycleSafetyEffectiveLength(double bicycleSafetyEffectiveLength) { elevationProfileSegment.setBicycleSafetyEffectiveLength(bicycleSafetyEffectiveLength); } public double getBicycleSafetyEffectiveLength() { return elevationProfileSegment.getBicycleSafetyEffectiveLength(); } private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); } @Override public PackedCoordinateSequence getElevationProfile(double start, double end) { return elevationProfileSegment.getElevationProfile(start, end); } public void setSlopeOverride(boolean slopeOverride) { elevationProfileSegment.setSlopeOverride(slopeOverride); } public void setNote(Set<Alert> notes) { this.notes = notes; } @Override public String toString() { return "PlainStreetEdge(" + getId() + ", " + name + ", " + fromv + " -> " + tov + " length=" + this.getLength() + " carSpeed=" + this.getCarSpeed() + " permission=" + this.getPermission() + ")"; } public boolean hasBogusName() { return hasBogusName; } /** Returns true if there are any turn restrictions defined. */ public boolean hasExplicitTurnRestrictions() { return this.turnRestrictions != null && this.turnRestrictions.size() > 0; } public void setWheelchairNote(Set<Alert> wheelchairNotes) { this.wheelchairNotes = wheelchairNotes; } public void addTurnRestriction(TurnRestriction turnRestriction) { if (turnRestrictions.isEmpty()) { turnRestrictions = new ArrayList<TurnRestriction>(); } turnRestrictions.add(turnRestriction); } @Override public PlainStreetEdge clone() { try { return (PlainStreetEdge) super.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } public boolean canTurnOnto(Edge e, State state, TraverseMode mode) { for (TurnRestriction restriction : turnRestrictions) { /* FIXME: This is wrong for trips that end in the middle of restriction.to */ // NOTE(flamholz): edge to be traversed decides equivalence. This is important since // it might be a temporary edge that is equivalent to some graph edge. if (restriction.type == TurnRestrictionType.ONLY_TURN) { if (!e.isEquivalentTo(restriction.to) && restriction.modes.contains(mode) && restriction.active(state.getTimeSeconds())) { return false; } } else { if (e.isEquivalentTo(restriction.to) && restriction.modes.contains(mode) && restriction.active(state.getTimeSeconds())) { return false; } } } return true; } @Override public ElevationProfileSegment getElevationProfileSegment() { return elevationProfileSegment; } protected boolean detachFrom() { for (Edge e : fromv.getIncoming()) { if (!(e instanceof PlainStreetEdge)) continue; PlainStreetEdge pse = (PlainStreetEdge) e; ArrayList<TurnRestriction> restrictions = new ArrayList<TurnRestriction>(pse.turnRestrictions); for (TurnRestriction restriction : restrictions) { if (restriction.to == this) { pse.turnRestrictions.remove(restriction); } } } return super.detachFrom(); } }