/* 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 com.google.common.collect.Iterables;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.LineString;
import org.opentripplanner.common.TurnRestriction;
import org.opentripplanner.common.TurnRestrictionType;
import org.opentripplanner.common.geometry.*;
import org.opentripplanner.common.model.P2;
import org.opentripplanner.routing.core.*;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.util.ElevationUtils;
import org.opentripplanner.routing.vertextype.BarrierVertex;
import org.opentripplanner.routing.vertextype.IntersectionVertex;
import org.opentripplanner.routing.vertextype.OsmVertex;
import org.opentripplanner.routing.vertextype.SplitterVertex;
import org.opentripplanner.routing.vertextype.StreetVertex;
import org.opentripplanner.routing.vertextype.TemporarySplitterVertex;
import org.opentripplanner.traffic.StreetSpeedSnapshot;
import org.opentripplanner.util.BitSetUtils;
import org.opentripplanner.util.I18NString;
import org.opentripplanner.util.NonLocalizedString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
/**
* This represents a street segment.
*
* @author novalis
*
*/
public class StreetEdge extends Edge implements Cloneable {
private static Logger LOG = LoggerFactory.getLogger(StreetEdge.class);
private static final long serialVersionUID = 1L;
/* TODO combine these with OSM highway= flags? */
public static final int CLASS_STREET = 3;
public static final int CLASS_CROSSING = 4;
public static final int CLASS_OTHERPATH = 5;
public static final int CLASS_OTHER_PLATFORM = 8;
public static final int CLASS_TRAIN_PLATFORM = 16;
public static final int ANY_PLATFORM_MASK = 24;
public static final int CROSSING_CLASS_MASK = 7; // ignore platform
public static final int CLASS_LINK = 32; // on/offramps; OSM calls them "links"
private static final double GREENWAY_SAFETY_FACTOR = 0.1;
// TODO(flamholz): do something smarter with the car speed here.
public static final float DEFAULT_CAR_SPEED = 11.2f;
/** If you have more than 8 flags, increase flags to short or int */
private static final int BACK_FLAG_INDEX = 0;
private static final int ROUNDABOUT_FLAG_INDEX = 1;
private static final int HASBOGUSNAME_FLAG_INDEX = 2;
private static final int NOTHRUTRAFFIC_FLAG_INDEX = 3;
private static final int STAIRS_FLAG_INDEX = 4;
private static final int SLOPEOVERRIDE_FLAG_INDEX = 5;
private static final int WHEELCHAIR_ACCESSIBLE_FLAG_INDEX = 6;
/** back, roundabout, stairs, ... */
private byte flags;
/**
* Length is stored internally as 32-bit fixed-point (millimeters). This allows edges of up to ~2100km.
* Distances used in calculations and exposed outside this class are still in double-precision floating point meters.
* Someday we might want to convert everything to fixed point representations.
*/
private int length_mm;
/**
* bicycleSafetyWeight = length * bicycleSafetyFactor. For example, a 100m street with a safety
* factor of 2.0 will be considered in term of safety cost as the same as a 150m street with a
* safety factor of 1.0.
*/
protected float bicycleSafetyFactor;
private int[] compactGeometry;
private I18NString name;
private StreetTraversalPermission permission;
/** The OSM way ID from whence this came - needed to reference traffic data */
public long wayId;
private int streetClass = CLASS_OTHERPATH;
/**
* The speed (meters / sec) at which an automobile can traverse
* this street segment.
*/
private float carSpeed;
/**
* The angle at the start of the edge geometry.
* Internal representation is -180 to +179 integer degrees mapped to -128 to +127 (brads)
*/
private byte inAngle;
/** The angle at the start of the edge geometry. Internal representation like that of inAngle. */
private byte outAngle;
public StreetEdge(StreetVertex v1, StreetVertex v2, LineString geometry,
I18NString name, double length,
StreetTraversalPermission permission, boolean back) {
super(v1, v2);
this.setBack(back);
this.setGeometry(geometry);
this.length_mm = (int) (length * 1000); // CONVERT FROM FLOAT METERS TO FIXED MILLIMETERS
this.bicycleSafetyFactor = 1.0f;
this.name = name;
this.setPermission(permission);
this.setCarSpeed(DEFAULT_CAR_SPEED);
this.setWheelchairAccessible(true); // accessible by default
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");
}
}
// Conversion from radians to internal representation as a single signed byte.
// We also reorient the angles since OTP seems to use South as a reference
// while the azimuth functions use North.
// FIXME Use only North as a reference, not a mix of North and South!
// Range restriction happens automatically due to Java signed overflow behavior.
// 180 degrees exists as a negative rather than a positive due to the integer range.
double angleRadians = DirectionUtils.getLastAngle(geometry);
outAngle = (byte) Math.round(angleRadians * 128 / Math.PI + 128);
angleRadians = DirectionUtils.getFirstAngle(geometry);
inAngle = (byte) Math.round(angleRadians * 128 / Math.PI + 128);
} 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;
}
}
}
//For testing only
public StreetEdge(StreetVertex v1, StreetVertex v2, LineString geometry,
String name, double length,
StreetTraversalPermission permission, boolean back) {
this(v1, v2, geometry, new NonLocalizedString(name), length, permission, back);
}
/**
* Checks permissions of the street edge if specified modes are allowed to travel.
*
* Barriers aren't taken into account. So it can happen that canTraverse returns True.
* But doTraverse returns false. Since there are barriers on a street.
*
* This is because this function is used also on street when searching for start/stop.
* Those streets are then split. On splitted streets can be possible to drive with a CAR because
* it is only blocked from one way.
* @param modes
* @return
*/
public boolean canTraverse(TraverseModeSet modes) {
return getPermission().allows(modes);
}
/**
* Checks if edge is accessible for wheelchair if needed according to tags or if slope is too big.
*
* Then it checks if street can be traversed according to street permissions and start/end barriers.
* This is done with intersection of street and barrier permissions in {@link #canTraverseIncludingBarrier(TraverseMode)}
*
* @param options
* @param mode
* @return
*/
private boolean canTraverse(RoutingRequest options, TraverseMode mode) {
if (options.wheelchairAccessible) {
if (!isWheelchairAccessible()) {
return false;
}
if (getMaxSlope() > options.maxSlope) {
return false;
}
}
return canTraverseIncludingBarrier(mode);
}
/**
* This checks if start or end vertex is bollard
* If it is it creates intersection of street edge permissions
* and from/to barriers.
* Then it checks if mode is allowed to traverse the edge.
*
* By default CAR isn't allowed to traverse barrier but foot and bicycle are.
* This can be changed with different tags
*
* If start/end isn't bollard it just checks the street permissions.
*
* It is used in {@link #canTraverse(RoutingRequest, TraverseMode)}
* @param mode
* @return
*/
public boolean canTraverseIncludingBarrier(TraverseMode mode) {
StreetTraversalPermission permission = getPermission();
if (fromv instanceof BarrierVertex) {
permission = permission.intersection(((BarrierVertex) fromv).getBarrierPermissions());
}
if (tov instanceof BarrierVertex) {
permission = permission.intersection(((BarrierVertex) tov).getBarrierPermissions());
}
return permission.allows(mode);
}
public PackedCoordinateSequence getElevationProfile() {
return null;
}
public boolean isElevationFlattened() {
return false;
}
public float getMaxSlope() {
return 0.0f;
}
@Override
public double getDistance() {
return length_mm / 1000.0; // CONVERT FROM FIXED MILLIMETERS TO FLOAT METERS
}
@Override
public State traverse(State s0) {
final RoutingRequest options = s0.getOptions();
final TraverseMode currMode = s0.getNonTransitMode();
StateEditor editor = doTraverse(s0, options, s0.getNonTransitMode());
State state = (editor == null) ? null : editor.makeState();
/* Kiss and ride support. Mode transitions occur without the explicit loop edges used in park-and-ride. */
if (options.kissAndRide) {
if (options.arriveBy) {
// Branch search to "unparked" CAR mode ASAP after transit has been used.
// Final WALK check prevents infinite recursion.
if (s0.isCarParked() && s0.isEverBoarded() && currMode == TraverseMode.WALK) {
editor = doTraverse(s0, options, TraverseMode.CAR);
if (editor != null) {
editor.setCarParked(false); // Also has the effect of switching to CAR
State forkState = editor.makeState();
if (forkState != null) {
forkState.addToExistingResultChain(state);
return forkState; // return both parked and unparked states
}
}
}
} else { /* departAfter */
// Irrevocable transition from driving to walking. "Parking" means being dropped off in this case.
// Final CAR check needed to prevent infinite recursion.
if ( ! s0.isCarParked() && ! getPermission().allows(TraverseMode.CAR) && currMode == TraverseMode.CAR) {
editor = doTraverse(s0, options, TraverseMode.WALK);
if (editor != null) {
editor.setCarParked(true); // has the effect of switching to WALK and preventing further car use
return editor.makeState(); // return only the "parked" walking state
}
}
}
}
return state;
}
/** return a StateEditor rather than a State so that we can make parking/mode switch modifications for kiss-and-ride. */
private StateEditor doTraverse(State s0, RoutingRequest options, TraverseMode traverseMode) {
boolean walkingBike = options.walkingBike;
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).
// TODO profiling indicates that this is a hot spot.
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);
/* Check whether this street allows the current mode. If not and we are biking, attempt to walk the bike. */
if (!canTraverse(options, traverseMode)) {
if (traverseMode == TraverseMode.BICYCLE) {
return doTraverse(s0, options.bikeWalkingOptions, TraverseMode.WALK);
}
return null;
}
// Automobiles have variable speeds depending on the edge type
double speed = calculateSpeed(options, traverseMode, s0.getTimeInMillis());
double time = getDistance() / speed;
double weight;
// TODO(flamholz): factor out this bike, wheelchair and walking specific logic to somewhere central.
if (options.wheelchairAccessible) {
weight = getSlopeSpeedEffectiveLength() / speed;
} else if (traverseMode.equals(TraverseMode.BICYCLE)) {
time = getSlopeSpeedEffectiveLength() / speed;
switch (options.optimize) {
case SAFE:
weight = bicycleSafetyFactor * getDistance() / speed;
break;
case GREENWAYS:
weight = bicycleSafetyFactor * getDistance() / speed;
if (bicycleSafetyFactor <= 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 = getDistance() / speed + getSlopeWorkCostEffectiveLength();
break;
case QUICK:
weight = getSlopeSpeedEffectiveLength() / speed;
break;
case TRIANGLE:
double quick = getSlopeSpeedEffectiveLength();
double safety = bicycleSafetyFactor * getDistance();
// TODO This computation is not coherent with the one for FLAT
double slope = getSlopeWorkCostEffectiveLength();
weight = quick * options.triangleTimeFactor + slope
* options.triangleSlopeFactor + safety
* options.triangleSafetyFactor;
weight /= speed;
break;
default:
weight = getDistance() / speed;
}
} else {
if (walkingBike) {
// take slopes into account when walking bikes
time = getSlopeSpeedEffectiveLength() / speed;
}
weight = time;
if (traverseMode.equals(TraverseMode.WALK)) {
// take slopes into account when walking
// FIXME: this causes steep stairs to be avoided. see #1297.
double costs = ElevationUtils.getWalkCostsForSlope(getDistance(), 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, elevationProfile.getMaxSlope(), costs, weight, timeflat);
}
*/
}
}
if (isStairs()) {
weight *= options.stairsReluctance;
} else {
// TODO: this is being applied even when biking or driving.
weight *= options.walkReluctance;
}
StateEditor s1 = s0.edit(this);
s1.setBackMode(traverseMode);
s1.setBackWalkingBike(walkingBike);
/* Handle no through traffic areas. */
if (this.isNoThruTraffic()) {
// Record transition into no-through-traffic area.
if (backEdge instanceof StreetEdge && !((StreetEdge)backEdge).isNoThruTraffic()) {
s1.setEnteredNoThroughTrafficArea();
}
// If we transitioned into a no-through-traffic area at some point, check if we are exiting it.
if (s1.hasEnteredNoThroughTrafficArea()) {
// Only Edges are marked as no-thru, but really we need to avoid creating dominant, pruned states
// on thru _Vertices_. This could certainly be improved somehow.
for (StreetEdge se : Iterables.filter(s1.getVertex().getOutgoing(), StreetEdge.class)) {
if (!se.isNoThruTraffic()) {
// This vertex has at least one through-traffic edge. We can't dominate it with a no-thru state.
return null;
}
}
}
}
/* Compute turn cost. */
StreetEdge backPSE;
if (backEdge != null && backEdge instanceof StreetEdge) {
backPSE = (StreetEdge) backEdge;
RoutingRequest backOptions = backWalkingBike ?
s0.getOptions().bikeWalkingOptions : s0.getOptions();
double backSpeed = backPSE.calculateSpeed(backOptions, backMode, s0.getTimeInMillis());
final double realTurnCost; // Units are seconds.
// Apply turn restrictions
if (options.arriveBy && !canTurnOnto(backPSE, s0, backMode)) {
return null;
} else if (!options.arriveBy && !backPSE.canTurnOnto(this, s0, traverseMode)) {
return null;
}
/*
* 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
IntersectionVertex traversedVertex = ((IntersectionVertex) tov);
realTurnCost = backOptions.getIntersectionTraversalCostModel().computeTraversalCost(
traversedVertex, this, backPSE, backMode, backOptions, (float) speed,
(float) backSpeed);
} else if (!options.arriveBy && fromv instanceof IntersectionVertex) { // depart-after search
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;
}
if (walkingBike || TraverseMode.BICYCLE.equals(traverseMode)) {
if (!(backWalkingBike || TraverseMode.BICYCLE.equals(backMode))) {
s1.incrementTimeInSeconds(options.bikeSwitchTime);
s1.incrementWeight(options.bikeSwitchCost);
}
}
if (!traverseMode.isDriving()) {
s1.incrementWalkDistance(getDistance());
}
/* On the pre-kiss/pre-park leg, limit both walking and driving, either soft or hard. */
int roundedTime = (int) Math.ceil(time);
if (options.kissAndRide || options.parkAndRide) {
if (options.arriveBy) {
if (!s0.isCarParked()) s1.incrementPreTransitTime(roundedTime);
} else {
if (!s0.isEverBoarded()) s1.incrementPreTransitTime(roundedTime);
}
if (s1.isMaxPreTransitTimeExceeded(options)) {
if (options.softPreTransitLimiting) {
weight += calculateOverageWeight(s0.getPreTransitTime(), s1.getPreTransitTime(),
options.maxPreTransitTime, options.preTransitPenalty,
options.preTransitOverageRate);
} else return null;
}
}
/* Apply a strategy for avoiding walking too far, either soft (weight increases) or hard limiting (pruning). */
if (s1.weHaveWalkedTooFar(options)) {
// if we're using a soft walk-limit
if( options.softWalkLimiting ){
// just slap a penalty for the overage onto s1
weight += calculateOverageWeight(s0.getWalkDistance(), s1.getWalkDistance(),
options.getMaxWalkDistance(), options.softWalkPenalty,
options.softWalkOverageRate);
} else {
// else, it's a hard limit; bail
LOG.debug("Too much walking. Bailing.");
return null;
}
}
s1.incrementTimeInSeconds(roundedTime);
s1.incrementWeight(weight);
return s1;
}
private double calculateOverageWeight(double firstValue, double secondValue, double maxValue,
double softPenalty, double overageRate) {
// apply penalty if we stepped over the limit on this traversal
boolean applyPenalty = false;
double overageValue;
if(firstValue <= maxValue && secondValue > maxValue){
applyPenalty = true;
overageValue = secondValue - maxValue;
} else {
overageValue = secondValue - firstValue;
}
// apply overage and add penalty if necessary
return (overageRate * overageValue) + (applyPenalty ? softPenalty : 0.0);
}
/**
* 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 getCarSpeed();
}
/**
* Calculate the speed appropriately given the RoutingRequest and traverseMode and the current wall clock time.
* Note: this is not strictly symmetrical, because in a forward search we get the speed based on the
* time we enter this edge, whereas in a reverse search we get the speed based on the time we exit
* the edge.
*/
public double calculateSpeed(RoutingRequest options, TraverseMode traverseMode, long timeMillis) {
if (traverseMode == null) {
return Double.NaN;
} else if (traverseMode.isDriving()) {
// NOTE: Automobiles have variable speeds depending on the edge type
if (options.useTraffic) {
// the expected speed based on traffic
StreetSpeedSnapshot source = options.getRoutingContext().streetSpeedSnapshot;
if (source != null) {
double congestedSpeed = source.getSpeed(this, traverseMode, timeMillis);
if (!Double.isNaN(congestedSpeed))
return congestedSpeed;
}
}
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.getDistance() / options.getStreetSpeedUpperBound();
}
public double getSlopeSpeedEffectiveLength() {
return getDistance();
}
public double getSlopeWorkCostEffectiveLength() {
return getDistance();
}
public void setBicycleSafetyFactor(float bicycleSafetyFactor) {
this.bicycleSafetyFactor = bicycleSafetyFactor;
}
public float getBicycleSafetyFactor() {
return bicycleSafetyFactor;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
}
public String toString() {
return "StreetEdge(" + getId() + ", " + name + ", " + fromv + " -> " + tov
+ " length=" + this.getDistance() + " carSpeed=" + this.getCarSpeed()
+ " permission=" + this.getPermission() + ")";
}
@Override
public StreetEdge clone() {
try {
return (StreetEdge) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
public boolean canTurnOnto(Edge e, State state, TraverseMode mode) {
for (TurnRestriction turnRestriction : getTurnRestrictions(state.getOptions().rctx.graph)) {
/* FIXME: This is wrong for trips that end in the middle of turnRestriction.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 (turnRestriction.type == TurnRestrictionType.ONLY_TURN) {
if (!e.isEquivalentTo(turnRestriction.to) && turnRestriction.modes.contains(mode) &&
turnRestriction.active(state.getTimeSeconds())) {
return false;
}
} else {
if (e.isEquivalentTo(turnRestriction.to) && turnRestriction.modes.contains(mode) &&
turnRestriction.active(state.getTimeSeconds())) {
return false;
}
}
}
return true;
}
@Override
public String getName() {
return this.name.toString();
}
/**
* Gets non-localized I18NString (Used when splitting edges)
* @return non-localized Name
*/
public I18NString getRawName() {
return this.name;
}
public String getName(Locale locale) {
return this.name.toString(locale);
}
public void setName(I18NString name) {
this.name = name;
}
public LineString getGeometry() {
return CompactLineString.uncompactLineString(fromv.getLon(), fromv.getLat(), tov.getLon(), tov.getLat(), compactGeometry, isBack());
}
private void setGeometry(LineString geometry) {
this.compactGeometry = CompactLineString.compactLineString(fromv.getLon(), fromv.getLat(), tov.getLon(), tov.getLat(), isBack() ? (LineString)geometry.reverse() : geometry, isBack());
}
public void shareData(StreetEdge reversedEdge) {
if (Arrays.equals(compactGeometry, reversedEdge.compactGeometry)) {
compactGeometry = reversedEdge.compactGeometry;
} else {
LOG.warn("Can't share geometry between {} and {}", this, reversedEdge);
}
}
public boolean isWheelchairAccessible() {
return BitSetUtils.get(flags, WHEELCHAIR_ACCESSIBLE_FLAG_INDEX);
}
public void setWheelchairAccessible(boolean wheelchairAccessible) {
flags = BitSetUtils.set(flags, WHEELCHAIR_ACCESSIBLE_FLAG_INDEX, wheelchairAccessible);
}
public StreetTraversalPermission getPermission() {
return permission;
}
public void setPermission(StreetTraversalPermission permission) {
this.permission = permission;
}
public int getStreetClass() {
return streetClass;
}
public void setStreetClass(int streetClass) {
this.streetClass = streetClass;
}
/**
* Marks that this edge is the reverse of the one defined in the source
* data. Does NOT mean fromv/tov are reversed.
*/
public boolean isBack() {
return BitSetUtils.get(flags, BACK_FLAG_INDEX);
}
public void setBack(boolean back) {
flags = BitSetUtils.set(flags, BACK_FLAG_INDEX, back);
}
public boolean isRoundabout() {
return BitSetUtils.get(flags, ROUNDABOUT_FLAG_INDEX);
}
public void setRoundabout(boolean roundabout) {
flags = BitSetUtils.set(flags, ROUNDABOUT_FLAG_INDEX, roundabout);
}
public boolean hasBogusName() {
return BitSetUtils.get(flags, HASBOGUSNAME_FLAG_INDEX);
}
public void setHasBogusName(boolean hasBogusName) {
flags = BitSetUtils.set(flags, HASBOGUSNAME_FLAG_INDEX, hasBogusName);
}
public boolean isNoThruTraffic() {
return BitSetUtils.get(flags, NOTHRUTRAFFIC_FLAG_INDEX);
}
public void setNoThruTraffic(boolean noThruTraffic) {
flags = BitSetUtils.set(flags, NOTHRUTRAFFIC_FLAG_INDEX, noThruTraffic);
}
/**
* This street is a staircase
*/
public boolean isStairs() {
return BitSetUtils.get(flags, STAIRS_FLAG_INDEX);
}
public void setStairs(boolean stairs) {
flags = BitSetUtils.set(flags, STAIRS_FLAG_INDEX, stairs);
}
public float getCarSpeed() {
return carSpeed;
}
public void setCarSpeed(float carSpeed) {
this.carSpeed = carSpeed;
}
public boolean isSlopeOverride() {
return BitSetUtils.get(flags, SLOPEOVERRIDE_FLAG_INDEX);
}
public void setSlopeOverride(boolean slopeOverride) {
flags = BitSetUtils.set(flags, SLOPEOVERRIDE_FLAG_INDEX, slopeOverride);
}
/**
* Return the azimuth of the first segment in this edge in integer degrees clockwise from South.
* TODO change everything to clockwise from North
*/
public int getInAngle() {
return this.inAngle * 180 / 128;
}
/** Return the azimuth of the last segment in this edge in integer degrees clockwise from South. */
public int getOutAngle() {
return this.outAngle * 180 / 128;
}
protected List<TurnRestriction> getTurnRestrictions(Graph graph) {
return graph.getTurnRestrictions(this);
}
/** calculate the length of this street segement from its geometry */
protected void calculateLengthFromGeometry () {
double accumulatedMeters = 0;
LineString geom = getGeometry();
for (int i = 1; i < geom.getNumPoints(); i++) {
accumulatedMeters += SphericalDistanceLibrary.distance(geom.getCoordinateN(i - 1), geom.getCoordinateN(i));
}
length_mm = (int) (accumulatedMeters * 1000);
}
/** Split this street edge and return the resulting street edges */
public P2<StreetEdge> split(SplitterVertex v, boolean destructive) {
P2<LineString> geoms = GeometryUtils.splitGeometryAtPoint(getGeometry(), v.getCoordinate());
StreetEdge e1 = null;
StreetEdge e2 = null;
if (destructive) {
e1 = new StreetEdge((StreetVertex) fromv, v, geoms.first, name, 0, permission, this.isBack());
e2 = new StreetEdge(v, (StreetVertex) tov, geoms.second, name, 0, permission, this.isBack());
// copy the wayId to the split edges, so we can trace them back to their parent if need be
e1.wayId = this.wayId;
e2.wayId = this.wayId;
// figure the lengths, ensuring that they sum to the length of this edge
e1.calculateLengthFromGeometry();
e2.calculateLengthFromGeometry();
// we have this code implemented in both directions, because splits are fudged half a millimeter
// when the length of this is odd. We want to make sure the lengths of the split streets end up
// exactly the same as their backStreets so that if they are split again the error does not accumulate
// and so that the order in which they are split does not matter.
if (!isBack()) {
// cast before the divide so that the sum is promoted
double frac = (double) e1.length_mm / (e1.length_mm + e2.length_mm);
e1.length_mm = (int) (length_mm * frac);
e2.length_mm = length_mm - e1.length_mm;
}
else {
// cast before the divide so that the sum is promoted
double frac = (double) e2.length_mm / (e1.length_mm + e2.length_mm);
e2.length_mm = (int) (length_mm * frac);
e1.length_mm = length_mm - e2.length_mm;
}
// TODO: better handle this temporary fix to handle bad edge distance calculation
if (e1.length_mm < 0) {
LOG.error("Edge 1 ({}) split at vertex at {},{} has length {} mm. Setting to 1 mm.", e1.wayId, v.getLat(), v.getLon(), e1.length_mm);
e1.length_mm = 1;
}
if (e2.length_mm < 0) {
LOG.error("Edge 2 ({}) split at vertex at {},{} has length {} mm. Setting to 1 mm.", e2.wayId, v.getLat(), v.getLon(), e2.length_mm);
e2.length_mm = 1;
}
if (e1.length_mm < 0 || e2.length_mm < 0) {
e1.tov.removeIncoming(e1);
e1.fromv.removeOutgoing(e1);
e2.tov.removeIncoming(e2);
e2.fromv.removeOutgoing(e2);
throw new IllegalStateException("Split street is longer than original street!");
}
for (StreetEdge e : new StreetEdge[] { e1, e2 }) {
e.setBicycleSafetyFactor(getBicycleSafetyFactor());
e.setHasBogusName(hasBogusName());
e.setStairs(isStairs());
e.setWheelchairAccessible(isWheelchairAccessible());
e.setBack(isBack());
}
} else {
if (((TemporarySplitterVertex) v).isEndVertex()) {
e1 = new TemporaryPartialStreetEdge(this, (StreetVertex) fromv, (TemporarySplitterVertex) v, geoms.first, name, 0);
e1.calculateLengthFromGeometry();
e1.setNoThruTraffic(this.isNoThruTraffic());
e1.setStreetClass(this.getStreetClass());
} else {
e2 = new TemporaryPartialStreetEdge(this, (TemporarySplitterVertex) v, (StreetVertex) tov, geoms.second, name, 0);
e2.calculateLengthFromGeometry();
e2.setNoThruTraffic(this.isNoThruTraffic());
e2.setStreetClass(this.getStreetClass());
}
}
return new P2<StreetEdge>(e1, e2);
}
/**
* Get the starting OSM node ID of this edge. Note that this information is preserved when an
* edge is split, so both edges will have the same starting and ending nodes.
*/
public long getStartOsmNodeId () {
if (fromv instanceof OsmVertex)
return ((OsmVertex) fromv).nodeId;
// get information from the splitter vertex so this edge gets the same traffic information it got before
// it was split.
else if (fromv instanceof SplitterVertex)
return ((SplitterVertex) fromv).previousNodeId;
else
return -1;
}
/**
* Get the ending OSM node ID of this edge. Note that this information is preserved when an
* edge is split, so both edges will have the same starting and ending nodes.
*/
public long getEndOsmNodeId () {
if (tov instanceof OsmVertex)
return ((OsmVertex) tov).nodeId;
// get information from the splitter vertex so this edge gets the same traffic information it got before
// it was split.
else if (tov instanceof SplitterVertex)
return ((SplitterVertex) tov).nextNodeId;
else
return -1;
}
}