/* 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.graph_builder.impl.osm;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.Setter;
import org.opentripplanner.common.DisjointSet;
import org.opentripplanner.common.RepeatingTimePeriod;
import org.opentripplanner.common.TurnRestriction;
import org.opentripplanner.common.TurnRestrictionType;
import org.opentripplanner.common.geometry.DistanceLibrary;
import org.opentripplanner.common.geometry.GeometryUtils;
import org.opentripplanner.common.geometry.SphericalDistanceLibrary;
import org.opentripplanner.common.model.P2;
import org.opentripplanner.gbannotation.ConflictingBikeTags;
import org.opentripplanner.gbannotation.Graphwide;
import org.opentripplanner.gbannotation.LevelAmbiguous;
import org.opentripplanner.gbannotation.StreetCarSpeedZero;
import org.opentripplanner.gbannotation.TurnRestrictionBad;
import org.opentripplanner.gbannotation.TurnRestrictionException;
import org.opentripplanner.gbannotation.TurnRestrictionUnknown;
import org.opentripplanner.graph_builder.impl.extra_elevation_data.ElevationPoint;
import org.opentripplanner.graph_builder.services.GraphBuilder;
import org.opentripplanner.graph_builder.services.osm.CustomNamer;
import org.opentripplanner.openstreetmap.model.OSMLevel;
import org.opentripplanner.openstreetmap.model.OSMLevel.Source;
import org.opentripplanner.openstreetmap.model.OSMNode;
import org.opentripplanner.openstreetmap.model.OSMRelation;
import org.opentripplanner.openstreetmap.model.OSMRelationMember;
import org.opentripplanner.openstreetmap.model.OSMTag;
import org.opentripplanner.openstreetmap.model.OSMWay;
import org.opentripplanner.openstreetmap.model.OSMWithTags;
import org.opentripplanner.openstreetmap.services.OpenStreetMapContentHandler;
import org.opentripplanner.openstreetmap.services.OpenStreetMapProvider;
import org.opentripplanner.routing.algorithm.GenericDijkstra;
import org.opentripplanner.routing.algorithm.strategies.SkipEdgeStrategy;
import org.opentripplanner.routing.bike_rental.BikeRentalStation;
import org.opentripplanner.routing.bike_rental.BikeRentalStationService;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.core.State;
import org.opentripplanner.routing.core.TraverseMode;
import org.opentripplanner.routing.core.TraverseModeSet;
import org.opentripplanner.routing.edgetype.AreaEdge;
import org.opentripplanner.routing.edgetype.AreaEdgeList;
import org.opentripplanner.routing.edgetype.ElevatorAlightEdge;
import org.opentripplanner.routing.edgetype.ElevatorBoardEdge;
import org.opentripplanner.routing.edgetype.ElevatorHopEdge;
import org.opentripplanner.routing.edgetype.FreeEdge;
import org.opentripplanner.routing.edgetype.NamedArea;
import org.opentripplanner.routing.edgetype.PlainStreetEdge;
import org.opentripplanner.routing.edgetype.RentABikeOffEdge;
import org.opentripplanner.routing.edgetype.RentABikeOnEdge;
import org.opentripplanner.routing.edgetype.StreetEdge;
import org.opentripplanner.routing.edgetype.StreetTraversalPermission;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.patch.Alert;
import org.opentripplanner.routing.patch.TranslatedString;
import org.opentripplanner.routing.spt.GraphPath;
import org.opentripplanner.routing.spt.ShortestPathTree;
import org.opentripplanner.routing.util.ElevationUtils;
import org.opentripplanner.routing.vertextype.BikeRentalStationVertex;
import org.opentripplanner.routing.vertextype.ElevatorOffboardVertex;
import org.opentripplanner.routing.vertextype.ElevatorOnboardVertex;
import org.opentripplanner.routing.vertextype.ExitVertex;
import org.opentripplanner.routing.vertextype.IntersectionVertex;
import org.opentripplanner.util.MapUtils;
import org.opentripplanner.visibility.Environment;
import org.opentripplanner.visibility.VLPoint;
import org.opentripplanner.visibility.VLPolygon;
import org.opentripplanner.visibility.VisibilityPolygon;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
* Builds a street graph from OpenStreetMap data.
*
*/
enum Direction {
LEFT, RIGHT, U, STRAIGHT;
}
/**
* A temporary holder for turn restrictions while we have only way/node ids but not yet edge objects
*/
class TurnRestrictionTag {
long via;
TurnRestrictionType type;
Direction direction;
RepeatingTimePeriod time;
public List<PlainStreetEdge> possibleFrom = new ArrayList<PlainStreetEdge>();
public List<PlainStreetEdge> possibleTo = new ArrayList<PlainStreetEdge>();
public TraverseModeSet modes;
TurnRestrictionTag(long via, TurnRestrictionType type, Direction direction) {
this.via = via;
this.type = type;
this.direction = direction;
}
@Override
public String toString() {
return String.format("%s turn restriction via node %d", direction, via);
}
}
public class OpenStreetMapGraphBuilderImpl implements GraphBuilder {
private static Logger LOG = LoggerFactory.getLogger(OpenStreetMapGraphBuilderImpl.class);
// Private members that are only read or written internally.
private Set<Object> _uniques = new HashSet<Object>();
private HashMap<Vertex, Double> elevationData = new HashMap<Vertex, Double>();
// Members that can be set by clients.
/**
* WayPropertySet computes edge properties from OSM way data.
*/
@Setter
private WayPropertySet wayPropertySet = new WayPropertySet();
/**
* Providers of OSM data.
*/
private List<OpenStreetMapProvider> _providers = new ArrayList<OpenStreetMapProvider>();
/**
* Allows for arbitrary custom naming of edges.
*/
@Setter
private CustomNamer customNamer;
/**
* Allows for alternate PlainStreetEdge implementations; this is intended for users who want to provide more info in PSE than OTP normally keeps
* around.
*/
@Setter
private OSMPlainStreetEdgeFactory edgeFactory = new DefaultOSMPlainStreetEdgeFactory();
/**
* If true, disallow zero floors and add 1 to non-negative numeric floors, as is generally done in the United States. This does not affect floor
* names from level maps.
*/
@Setter
private boolean noZeroLevels = true;
/**
* Whether bike rental stations should be loaded from OSM, rather than periodically dynamically pulled from APIs.
*/
@Setter
private boolean staticBikeRental = false;
public List<String> provides() {
return Arrays.asList("streets", "turns");
}
public List<String> getPrerequisites() {
return Collections.emptyList();
}
/**
* The source for OSM map data
*/
public void setProvider(OpenStreetMapProvider provider) {
_providers.add(provider);
}
/**
* Multiple sources for OSM map data
*/
public void setProviders(List<OpenStreetMapProvider> providers) {
_providers.addAll(providers);
}
/**
* Set the way properties from a {@link WayPropertySetSource} source.
*
* @param source the way properties source
*/
public void setDefaultWayPropertySetSource(WayPropertySetSource source) {
wayPropertySet = source.getWayPropertySet();
}
@Override
public void buildGraph(Graph graph, HashMap<Class<?>, Object> extra) {
Handler handler = new Handler(graph);
for (OpenStreetMapProvider provider : _providers) {
LOG.info("Gathering OSM from provider: " + provider);
provider.readOSM(handler);
}
LOG.info("Building street graph from OSM");
handler.buildGraph(extra);
}
@SuppressWarnings("unchecked")
private <T> T unique(T value) {
if (!_uniques.contains(value)) {
_uniques.add(value);
}
return (T) value;
}
private class Handler implements OpenStreetMapContentHandler {
private static final double VISIBILITY_EPSILON = 0.000000001;
private static final String nodeLabelFormat = "osm:node:%d";
private static final String levelnodeLabelFormat = nodeLabelFormat + ":level:%s";
private Map<Long, OSMNode> _nodes = new HashMap<Long, OSMNode>();
private Map<Long, OSMWay> _ways = new HashMap<Long, OSMWay>();
private List<Area> _areas = new ArrayList<Area>();
private Set<Long> _areaWayIds = new HashSet<Long>();
private Map<Long, OSMWay> _areaWaysById = new HashMap<Long, OSMWay>();
private Map<Long, Set<OSMWay>> _areasForNode = new HashMap<Long, Set<OSMWay>>();
private List<OSMWay> _singleWayAreas = new ArrayList<OSMWay>();
private Map<Long, OSMRelation> _relations = new HashMap<Long, OSMRelation>();
private Set<OSMWithTags> _processedAreas = new HashSet<OSMWithTags>();
private Set<Long> _nodesWithNeighbors = new HashSet<Long>();
private Set<Long> _areaNodes = new HashSet<Long>();
private Map<Long, List<TurnRestrictionTag>> turnRestrictionsByFromWay = new HashMap<Long, List<TurnRestrictionTag>>();
private Map<Long, List<TurnRestrictionTag>> turnRestrictionsByToWay = new HashMap<Long, List<TurnRestrictionTag>>();
class Ring {
public List<OSMNode> nodes;
public VLPolygon geometry;
public List<Ring> holes = new ArrayList<Ring>();
// equivalent to the ring representation, but used for JTS operations
private Polygon jtsPolygon;
/**
* Why is there a boolean parameter called javaSucks? Because otherwise the two constructors have the same erasure, meaning that even
* though Java has enough information at compile-time to figure out which constructor I am talking about, it intentionally throws this
* away in the interest of having worse run-time performance. Thanks, Java!
*
* Oh, and most people would solve this problem by making a static factory method but that won't work because then all of this class's
* outer classes would have to be static.
*
* @param osmNodes
* @param javaSucks
*/
public Ring(List<OSMNode> osmNodes, boolean javaSucks) {
ArrayList<VLPoint> vertices = new ArrayList<VLPoint>();
nodes = osmNodes;
for (OSMNode node : osmNodes) {
VLPoint point = new VLPoint(node.getLon(), node.getLat());
vertices.add(point);
}
geometry = new VLPolygon(vertices);
}
public Ring(List<Long> osmNodes) {
ArrayList<VLPoint> vertices = new ArrayList<VLPoint>();
nodes = new ArrayList<OSMNode>(osmNodes.size());
for (long nodeId : osmNodes) {
OSMNode node = _nodes.get(nodeId);
if (nodes.contains(node)) {
// hopefully, this only happens in order to
// close polygons
continue;
}
VLPoint point = new VLPoint(node.getLon(), node.getLat());
nodes.add(node);
vertices.add(point);
}
geometry = new VLPolygon(vertices);
}
public Polygon toJtsPolygon() {
if (jtsPolygon != null) {
return jtsPolygon;
}
GeometryFactory factory = GeometryUtils.getGeometryFactory();
LinearRing shell = factory.createLinearRing(toCoordinates(geometry));
// we need to merge connected holes here, because JTS does not believe in
// holes that touch at multiple points (and, weirdly, does not have a method
// to detect this other than this crazy DE-9IM stuff
List<Polygon> polygonHoles = new ArrayList<Polygon>();
for (Ring ring : holes) {
LinearRing linearRing = factory.createLinearRing(toCoordinates(ring.geometry));
Polygon polygon = factory.createPolygon(linearRing, new LinearRing[0]);
for (Iterator<Polygon> it = polygonHoles.iterator(); it.hasNext();) {
Polygon otherHole = it.next();
if (otherHole.relate(polygon, "F***1****")) {
polygon = (Polygon) polygon.union(otherHole);
it.remove();
}
}
polygonHoles.add(polygon);
}
ArrayList<LinearRing> lrholelist = new ArrayList<LinearRing>(polygonHoles.size());
for (Polygon hole : polygonHoles) {
Geometry boundary = hole.getBoundary();
if (boundary instanceof LinearRing) {
lrholelist.add((LinearRing) boundary);
} else {
// this is a case of a hole inside a hole. OSM technically
// allows this, but it would be a giant hassle to get right. So:
LineString line = hole.getExteriorRing();
LinearRing ring = factory.createLinearRing(line.getCoordinates());
lrholelist.add(ring);
}
}
LinearRing[] lrholes = lrholelist.toArray(new LinearRing[lrholelist.size()]);
jtsPolygon = factory.createPolygon(shell, lrholes);
return jtsPolygon;
}
private Coordinate[] toCoordinates(VLPolygon geometry) {
Coordinate[] coords = new Coordinate[geometry.n() + 1];
int i = 0;
for (VLPoint point : geometry.vertices) {
coords[i++] = new Coordinate(point.x, point.y);
}
VLPoint first = geometry.vertices.get(0);
coords[i++] = new Coordinate(first.x, first.y);
return coords;
}
}
/**
* Stores information about an OSM area needed for visibility graph construction. Algorithm based on
* http://wiki.openstreetmap.org/wiki/Relation:multipolygon/Algorithm but generally done in a quick/dirty way.
*/
class Area {
public class AreaConstructionException extends RuntimeException {
private static final long serialVersionUID = 1L;
}
OSMWithTags parent; // this is the way or relation that has the relevant tags for the
// area
List<Ring> outermostRings = new ArrayList<Ring>();
private MultiPolygon jtsMultiPolygon;
Area(OSMWithTags parent, List<OSMWay> outerRingWays, List<OSMWay> innerRingWays) {
this.parent = parent;
setWayName(parent);
// ring assignment
List<List<Long>> innerRingNodes = constructRings(innerRingWays);
List<List<Long>> outerRingNodes = constructRings(outerRingWays);
if (innerRingNodes == null || outerRingNodes == null) {
throw new AreaConstructionException();
}
ArrayList<List<Long>> allRings = new ArrayList<List<Long>>(innerRingNodes);
allRings.addAll(outerRingNodes);
List<Ring> innerRings = new ArrayList<Ring>();
List<Ring> outerRings = new ArrayList<Ring>();
for (List<Long> ring : innerRingNodes) {
innerRings.add(new Ring(ring));
}
for (List<Long> ring : outerRingNodes) {
outerRings.add(new Ring(ring));
}
// now, ring grouping
// first, find outermost rings
OUTER: for (Ring outer : outerRings) {
for (Ring possibleContainer : outerRings) {
if (outer != possibleContainer
&& outer.geometry.hasPointInside(possibleContainer.geometry)) {
continue OUTER;
}
}
outermostRings.add(outer);
// find holes in this ring
for (Ring possibleHole : innerRings) {
if (possibleHole.geometry.hasPointInside(outer.geometry)) {
outer.holes.add(possibleHole);
}
}
}
// run this at end of ctor so that exception
// can be caught in the right place
toJTSMultiPolygon();
}
public MultiPolygon toJTSMultiPolygon() {
if (jtsMultiPolygon == null) {
List<Polygon> polygons = new ArrayList<Polygon>();
for (Ring ring : outermostRings) {
polygons.add(ring.toJtsPolygon());
}
jtsMultiPolygon = GeometryUtils.getGeometryFactory().createMultiPolygon(
polygons.toArray(new Polygon[0]));
if (!jtsMultiPolygon.isValid()) {
throw new AreaConstructionException();
}
}
return jtsMultiPolygon;
}
public List<List<Long>> constructRings(List<OSMWay> ways) {
if (ways.size() == 0) {
// no rings is no rings
return Collections.emptyList();
}
List<List<Long>> closedRings = new ArrayList<List<Long>>();
HashMap<Long, List<OSMWay>> waysByEndpoint = new HashMap<Long, List<OSMWay>>();
for (OSMWay way : ways) {
List<Long> refs = way.getNodeRefs();
long start = refs.get(0);
long end = refs.get(refs.size() - 1);
if (start == end) {
ArrayList<Long> ring = new ArrayList<Long>(refs);
closedRings.add(ring);
} else {
MapUtils.addToMapList(waysByEndpoint, start, way);
MapUtils.addToMapList(waysByEndpoint, end, way);
}
}
// precheck for impossible situations
List<Long> toRemove = new ArrayList<Long>();
for (Map.Entry<Long, List<OSMWay>> entry : waysByEndpoint.entrySet()) {
List<OSMWay> list = entry.getValue();
if (list.size() % 2 == 1) {
return null;
}
}
for (Long key : toRemove) {
waysByEndpoint.remove(key);
}
List<Long> partialRing = new ArrayList<Long>();
if (waysByEndpoint.size() == 0) {
return closedRings;
}
long firstEndpoint = 0, otherEndpoint = 0;
OSMWay firstWay = null;
for (Map.Entry<Long, List<OSMWay>> entry : waysByEndpoint.entrySet()) {
firstEndpoint = entry.getKey();
List<OSMWay> list = entry.getValue();
firstWay = list.get(0);
List<Long> nodeRefs = firstWay.getNodeRefs();
partialRing.addAll(nodeRefs);
firstEndpoint = nodeRefs.get(0);
otherEndpoint = nodeRefs.get(nodeRefs.size() - 1);
break;
}
waysByEndpoint.get(firstEndpoint).remove(firstWay);
waysByEndpoint.get(otherEndpoint).remove(firstWay);
if (constructRingsRecursive(waysByEndpoint, partialRing, closedRings, firstEndpoint)) {
return closedRings;
} else {
return null;
}
}
private boolean constructRingsRecursive(HashMap<Long, List<OSMWay>> waysByEndpoint,
List<Long> ring, List<List<Long>> closedRings, long endpoint) {
List<OSMWay> ways = new ArrayList<OSMWay>(waysByEndpoint.get(endpoint));
for (OSMWay way : ways) {
// remove this way from the map
List<Long> nodeRefs = way.getNodeRefs();
long firstEndpoint = nodeRefs.get(0);
long otherEndpoint = nodeRefs.get(nodeRefs.size() - 1);
MapUtils.removeFromMapList(waysByEndpoint, firstEndpoint, way);
MapUtils.removeFromMapList(waysByEndpoint, otherEndpoint, way);
ArrayList<Long> newRing = new ArrayList<Long>(ring.size() + nodeRefs.size());
long newFirstEndpoint;
if (firstEndpoint == endpoint) {
for (int j = nodeRefs.size() - 1; j >= 1; --j) {
newRing.add(nodeRefs.get(j));
}
newRing.addAll(ring);
newFirstEndpoint = otherEndpoint;
} else {
newRing.addAll(nodeRefs.subList(0, nodeRefs.size() - 1));
newRing.addAll(ring);
newFirstEndpoint = firstEndpoint;
}
if (newRing.get(newRing.size() - 1).equals(newRing.get(0))) {
// ring closure
closedRings.add(newRing);
// if we're out of endpoints, then we have succeeded
if (waysByEndpoint.size() == 0) {
return true; // success
}
// otherwise, we need to start a new partial ring
newRing = new ArrayList<Long>();
OSMWay firstWay = null;
for (Map.Entry<Long, List<OSMWay>> entry : waysByEndpoint.entrySet()) {
firstEndpoint = entry.getKey();
List<OSMWay> list = entry.getValue();
firstWay = list.get(0);
nodeRefs = firstWay.getNodeRefs();
newRing.addAll(nodeRefs);
firstEndpoint = nodeRefs.get(0);
otherEndpoint = nodeRefs.get(nodeRefs.size() - 1);
break;
}
MapUtils.removeFromMapList(waysByEndpoint, firstEndpoint, firstWay);
MapUtils.removeFromMapList(waysByEndpoint, otherEndpoint, firstWay);
if (constructRingsRecursive(waysByEndpoint, newRing, closedRings,
firstEndpoint)) {
return true;
}
MapUtils.addToMapList(waysByEndpoint, firstEndpoint, firstWay);
MapUtils.addToMapList(waysByEndpoint, otherEndpoint, firstWay);
} else {
// continue with this ring
if (waysByEndpoint.get(newFirstEndpoint) != null) {
if (constructRingsRecursive(waysByEndpoint, newRing, closedRings,
newFirstEndpoint)) {
return true;
}
}
}
if (firstEndpoint == endpoint) {
MapUtils.addToMapList(waysByEndpoint, otherEndpoint, way);
} else {
MapUtils.addToMapList(waysByEndpoint, firstEndpoint, way);
}
}
return false;
}
}
/**
* A group of possibly-contiguous areas sharing the same level
*/
class AreaGroup {
/*
* The list of underlying areas, used when generating edges out of the visibility graph
*/
Collection<Area> areas;
/**
* The joined outermost rings of the areas (with inner rings for holes as necessary).
*/
List<Ring> outermostRings = new ArrayList<Ring>();
public AreaGroup(Collection<Area> areas) {
this.areas = areas;
// Merging non-convex polygons is complicated, so we need to convert to JTS, let JTS do the hard work,
// then convert back.
List<Polygon> allRings = new ArrayList<Polygon>();
// However, JTS will lose the coord<->osmnode mapping, and we will have to reconstruct it.
HashMap<Coordinate, OSMNode> nodeMap = new HashMap<Coordinate, OSMNode>();
for (Area area : areas) {
for (Ring ring : area.outermostRings) {
allRings.add(ring.toJtsPolygon());
for (OSMNode node : ring.nodes) {
nodeMap.put(new Coordinate(node.getLon(), node.getLat()), node);
}
for (Ring inner : ring.holes) {
for (OSMNode node : inner.nodes) {
nodeMap.put(new Coordinate(node.getLon(), node.getLat()), node);
}
}
}
}
GeometryFactory geometryFactory = GeometryUtils.getGeometryFactory();
Geometry u = geometryFactory.createMultiPolygon(allRings
.toArray(new Polygon[allRings.size()]));
u = u.union();
if (u instanceof GeometryCollection) {
GeometryCollection mp = (GeometryCollection) u;
for (int i = 0; i < mp.getNumGeometries(); ++i) {
Geometry poly = mp.getGeometryN(i);
if (!(poly instanceof Polygon)) {
LOG.warn("Unexpected non-polygon when merging areas: " + poly);
continue;
}
outermostRings.add(toRing((Polygon) poly, nodeMap));
}
} else if (u instanceof Polygon) {
outermostRings.add(toRing((Polygon) u, nodeMap));
} else {
LOG.warn("Unexpected non-polygon when merging areas: " + u);
}
}
public class RingConstructionException extends RuntimeException {
private static final long serialVersionUID = 1L;
}
private Ring toRing(Polygon polygon, HashMap<Coordinate, OSMNode> nodeMap) {
List<OSMNode> shell = new ArrayList<OSMNode>();
for (Coordinate coord : polygon.getExteriorRing().getCoordinates()) {
OSMNode node = nodeMap.get(coord);
if (node == null) {
throw new RingConstructionException();
}
shell.add(node);
}
Ring ring = new Ring(shell, true);
// now the holes
for (int i = 0; i < polygon.getNumInteriorRing(); ++i) {
LineString interior = polygon.getInteriorRingN(i);
List<OSMNode> hole = new ArrayList<OSMNode>();
for (Coordinate coord : interior.getCoordinates()) {
OSMNode node = nodeMap.get(coord);
if (node == null) {
throw new RingConstructionException();
}
hole.add(node);
}
ring.holes.add(new Ring(hole, true));
}
return ring;
}
public OSMWithTags getSomeOSMObject() {
return areas.iterator().next().parent;
}
}
private Graph graph;
/** The bike safety factor of the safest street */
private double bestBikeSafety = 1;
// track OSM nodes which are decomposed into multiple graph vertices because they are
// elevators. later they will be iterated over to build ElevatorEdges between them.
private HashMap<Long, HashMap<OSMLevel, IntersectionVertex>> multiLevelNodes = new HashMap<Long, HashMap<OSMLevel, IntersectionVertex>>();
// track OSM nodes that will become graph vertices because they appear in multiple OSM ways
private Map<Long, IntersectionVertex> intersectionNodes = new HashMap<Long, IntersectionVertex>();
// track vertices to be removed in the turn-graph conversion.
// this is a superset of intersectionNodes.values, which contains
// a null vertex reference for multilevel nodes. the individual vertices
// for each level of a multilevel node are includeed in endpoints.
private ArrayList<IntersectionVertex> endpoints = new ArrayList<IntersectionVertex>();
// track which vertical level each OSM way belongs to, for building elevators etc.
private Map<OSMWithTags, OSMLevel> wayLevels = new HashMap<OSMWithTags, OSMLevel>();
private HashSet<OSMNode> _bikeRentalNodes = new HashSet<OSMNode>();
private DistanceLibrary distanceLibrary = SphericalDistanceLibrary.getInstance();
private HashMap<Coordinate, IntersectionVertex> areaBoundaryVertexForCoordinate = new HashMap<Coordinate, IntersectionVertex>();
public Handler(Graph graph) {
this.graph = graph;
}
public void buildGraph(HashMap<Class<?>, Object> extra) {
// handle turn restrictions, road names, and level maps in relations
processRelations();
if (staticBikeRental) {
processBikeRentalNodes();
}
// Remove all simple islands
HashSet<Long> _keep = new HashSet<Long>(_nodesWithNeighbors);
_keep.addAll(_areaNodes);
_nodes.keySet().retainAll(_keep);
// figure out which nodes that are actually intersections
initIntersectionNodes();
buildBasicGraph();
buildAreas();
buildElevatorEdges(graph);
/* unify turn restrictions */
for (List<TurnRestrictionTag> restrictions : turnRestrictionsByFromWay.values()) {
for (TurnRestrictionTag restrictionTag : restrictions) {
if (restrictionTag.possibleFrom.isEmpty()) {
LOG.warn("No from edge found for " + restrictionTag);
continue;
}
if (restrictionTag.possibleTo.isEmpty()) {
LOG.warn("No to edge found for " + restrictionTag);
continue;
}
for (PlainStreetEdge from : restrictionTag.possibleFrom) {
if (from == null) {
LOG.warn("from-edge is null in turn " + restrictionTag);
continue;
}
for (PlainStreetEdge to : restrictionTag.possibleTo) {
if (from == null || to == null) {
continue;
}
int angleDiff = from.getOutAngle() - to.getInAngle();
if (angleDiff < 0) {
angleDiff += 360;
}
switch (restrictionTag.direction) {
case LEFT:
if (angleDiff >= 160) {
continue; // not a left turn
}
break;
case RIGHT:
if (angleDiff <= 200)
continue; // not a right turn
break;
case U:
if ((angleDiff <= 150 || angleDiff > 210))
continue; // not a U turn
break;
case STRAIGHT:
if (angleDiff >= 30 && angleDiff < 330)
continue; // not straight
break;
}
TurnRestriction restriction = new TurnRestriction();
restriction.from = from;
restriction.to = to;
restriction.type = restrictionTag.type;
restriction.modes = restrictionTag.modes;
restriction.time = restrictionTag.time;
from.addTurnRestriction(restriction);
}
}
}
}
if (customNamer != null) {
customNamer.postprocess(graph);
}
// generate elevation profiles
extra.put(ElevationPoint.class, elevationData);
applyBikeSafetyFactor(graph);
} // END buildGraph()
private void processBikeRentalNodes() {
LOG.info("Processing bike rental nodes...");
int n = 0;
BikeRentalStationService bikeRentalService = new BikeRentalStationService();
graph.putService(BikeRentalStationService.class, bikeRentalService);
for (OSMNode node : _bikeRentalNodes) {
n++;
String creativeName = wayPropertySet.getCreativeNameForWay(node);
int capacity = Integer.MAX_VALUE;
if (node.hasTag("capacity")) {
try {
capacity = node.getCapacity();
} catch (NumberFormatException e) {
LOG.warn("Capacity for osm node " + node.getId() + " (" + creativeName
+ ") is not a number: " + node.getTag("capacity"));
}
}
String networks = node.getTag("network");
String operators = node.getTag("operator");
Set<String> networkSet = new HashSet<String>();
if (networks != null)
networkSet.addAll(Arrays.asList(networks.split(";")));
if (operators != null)
networkSet.addAll(Arrays.asList(operators.split(";")));
if (networkSet.isEmpty()) {
LOG.warn("Bike rental station at osm node " + node.getId() + " ("
+ creativeName + ") with no network; including as compatible-with-all.");
networkSet.add("*"); // Special "catch-all" value
}
BikeRentalStation station = new BikeRentalStation();
station.id = "" + node.getId();
station.name = creativeName;
station.x = node.getLon();
station.y = node.getLat();
// The following make sure that spaces+bikes=capacity, always.
// Also, for the degenerate case of capacity=1, we should have 1
// bike available, not 0.
station.spacesAvailable = capacity / 2;
station.bikesAvailable = capacity - station.spacesAvailable;
station.realTimeData = false;
bikeRentalService.addStation(station);
BikeRentalStationVertex stationVertex = new BikeRentalStationVertex(graph, station);
new RentABikeOnEdge(stationVertex, stationVertex, networkSet);
new RentABikeOffEdge(stationVertex, stationVertex, networkSet);
}
LOG.info("Created " + n + " bike rental stations.");
}
final int MAX_AREA_NODES = 500;
private void buildAreas() {
LOG.info("Building visibility graphs for areas");
List<AreaGroup> areaGroups = groupAreas(_areas);
for (AreaGroup group : areaGroups) {
buildAreasForGroup(group);
}
}
/**
* Theoretically, it is not correct to build the visibility graph on the joined polygon of areas with different levels of bike safety. That's
* because in the optimal path, you might end up changing direction at area boundaries. The problem is known as "weighted planar
* subdivisions", and the best known algorithm is O(N^3). That's not much worse than general visibility graph construction, but it would have
* to be done at runtime to account for the differences in bike safety preferences. Ted Chiang's "Story Of Your Life" describes how a very
* similar problem in optics gives rise to Snell's Law. It is the second-best story about a law of physics that I know of (Chiang's
* "Exhalation" is the first).
*
* Anyway, since we're not going to run an O(N^3) algorithm at runtime just to give people who don't understand Snell's Law weird paths that
* they can complain about, this should be just fine.
*
* @param group
*/
private void buildAreasForGroup(AreaGroup group) {
Set<OSMNode> startingNodes = new HashSet<OSMNode>();
Set<Vertex> startingVertices = new HashSet<Vertex>();
Set<Edge> edges = new HashSet<Edge>();
// create polygon and accumulate nodes for area
for (Ring ring : group.outermostRings) {
AreaEdgeList edgeList = new AreaEdgeList();
// the points corresponding to concave or hole vertices
// or those linked to ways
ArrayList<VLPoint> visibilityPoints = new ArrayList<VLPoint>();
ArrayList<OSMNode> visibilityNodes = new ArrayList<OSMNode>();
HashSet<P2<OSMNode>> alreadyAddedEdges = new HashSet<P2<OSMNode>>();
// we need to accumulate visibility points from all contained areas
// inside this ring, but only for shared nodes; we don't care about
// convexity, which we'll handle for the grouped area only.
// we also want to fill in the edges of this area anyway, because we can,
// and to avoid the numerical problems that they tend to cause
for (Area area : group.areas) {
if (!ring.toJtsPolygon().contains(area.toJTSMultiPolygon())) {
continue;
}
for (Ring outerRing : area.outermostRings) {
for (int i = 0; i < outerRing.nodes.size(); ++i) {
OSMNode node = outerRing.nodes.get(i);
createEdgesForRingSegment(edges, edgeList, area, outerRing, i,
alreadyAddedEdges);
addtoVisibilityAndStartSets(startingNodes, visibilityPoints,
visibilityNodes, node);
}
for (Ring innerRing : outerRing.holes) {
for (int j = 0; j < innerRing.nodes.size(); ++j) {
OSMNode node = innerRing.nodes.get(j);
createEdgesForRingSegment(edges, edgeList, area, innerRing, j,
alreadyAddedEdges);
addtoVisibilityAndStartSets(startingNodes, visibilityPoints,
visibilityNodes, node);
}
}
}
}
List<OSMNode> nodes = new ArrayList<OSMNode>();
List<VLPoint> vertices = new ArrayList<VLPoint>();
accumulateRingNodes(ring, nodes, vertices);
VLPolygon polygon = makeStandardizedVLPolygon(vertices, nodes, false);
accumulateVisibilityPoints(ring.nodes, polygon, visibilityPoints, visibilityNodes,
false);
ArrayList<VLPolygon> polygons = new ArrayList<VLPolygon>();
polygons.add(polygon);
// holes
for (Ring innerRing : ring.holes) {
ArrayList<OSMNode> holeNodes = new ArrayList<OSMNode>();
vertices = new ArrayList<VLPoint>();
accumulateRingNodes(innerRing, holeNodes, vertices);
VLPolygon hole = makeStandardizedVLPolygon(vertices, holeNodes, true);
accumulateVisibilityPoints(innerRing.nodes, hole, visibilityPoints,
visibilityNodes, true);
nodes.addAll(holeNodes);
polygons.add(hole);
}
Environment areaEnv = new Environment(polygons);
// FIXME: temporary hard limit on size of
// areas to prevent way explosion
if (visibilityPoints.size() > MAX_AREA_NODES) {
LOG.warn("Area " + group.getSomeOSMObject() + " is too complicated ("
+ visibilityPoints.size() + " > " + MAX_AREA_NODES);
continue;
}
if (!areaEnv.is_valid(VISIBILITY_EPSILON)) {
LOG.warn("Area " + group.getSomeOSMObject()
+ " is not epsilon-valid (epsilon = " + VISIBILITY_EPSILON + ")");
continue;
}
edgeList.setOriginalEdges(ring.toJtsPolygon());
createNamedAreas(edgeList, ring, group.areas);
OSMWithTags areaEntity = group.getSomeOSMObject();
for (int i = 0; i < visibilityNodes.size(); ++i) {
OSMNode nodeI = visibilityNodes.get(i);
VisibilityPolygon visibilityPolygon = new VisibilityPolygon(
visibilityPoints.get(i), areaEnv, VISIBILITY_EPSILON);
Polygon poly = toJTSPolygon(visibilityPolygon);
for (int j = 0; j < visibilityNodes.size(); ++j) {
OSMNode nodeJ = visibilityNodes.get(j);
P2<OSMNode> nodePair = new P2<OSMNode>(nodeI, nodeJ);
if (alreadyAddedEdges.contains(nodePair))
continue;
IntersectionVertex startEndpoint = getVertexForOsmNode(nodeI, areaEntity);
IntersectionVertex endEndpoint = getVertexForOsmNode(nodeJ, areaEntity);
Coordinate[] coordinates = new Coordinate[] {
startEndpoint.getCoordinate(), endEndpoint.getCoordinate() };
GeometryFactory geometryFactory = GeometryUtils.getGeometryFactory();
LineString line = geometryFactory.createLineString(coordinates);
if (poly.contains(line)) {
createSegments(nodeI, nodeJ, startEndpoint, endEndpoint, group.areas,
edgeList, edges);
if (startingNodes.contains(nodeI)) {
startingVertices.add(startEndpoint);
}
if (startingNodes.contains(nodeJ)) {
startingVertices.add(endEndpoint);
}
}
}
}
}
pruneAreaEdges(startingVertices, edges);
}
private void addtoVisibilityAndStartSets(Set<OSMNode> startingNodes,
ArrayList<VLPoint> visibilityPoints, ArrayList<OSMNode> visibilityNodes,
OSMNode node) {
if (_nodesWithNeighbors.contains(node.getId()) || multipleAreasContain(node.getId())) {
startingNodes.add(node);
VLPoint point = new VLPoint(node.getLon(), node.getLat());
if (!visibilityPoints.contains(point)) {
visibilityPoints.add(point);
visibilityNodes.add(node);
}
}
}
private Polygon toJTSPolygon(VLPolygon visibilityPolygon) {
// incomprehensibly, visilibity's routines for figuring out point-polygon containment are too broken
// to use here, so we have to fall back to JTS.
Coordinate[] coordinates = new Coordinate[visibilityPolygon.n() + 1];
for (int p = 0; p < coordinates.length; ++p) {
VLPoint vlPoint = visibilityPolygon.get(p);
coordinates[p] = new Coordinate(vlPoint.x, vlPoint.y);
}
LinearRing shell = GeometryUtils.getGeometryFactory().createLinearRing(coordinates);
Polygon poly = GeometryUtils.getGeometryFactory().createPolygon(shell,
new LinearRing[0]);
return poly;
}
private void createEdgesForRingSegment(Set<Edge> edges, AreaEdgeList edgeList, Area area,
Ring ring, int i, HashSet<P2<OSMNode>> alreadyAddedEdges) {
OSMNode node = ring.nodes.get(i);
OSMNode nextNode = ring.nodes.get((i + 1) % ring.nodes.size());
P2<OSMNode> nodePair = new P2<OSMNode>(node, nextNode);
if (alreadyAddedEdges.contains(nodePair)) {
return;
}
alreadyAddedEdges.add(nodePair);
IntersectionVertex startEndpoint = getVertexForOsmNode(node, area.parent);
IntersectionVertex endEndpoint = getVertexForOsmNode(nextNode, area.parent);
createSegments(node, nextNode, startEndpoint, endEndpoint, Arrays.asList(area),
edgeList, edges);
}
private void createNamedAreas(AreaEdgeList edgeList, Ring ring, Collection<Area> areas) {
Polygon containingArea = ring.toJtsPolygon();
for (Area area : areas) {
Geometry intersection = containingArea.intersection(area.toJTSMultiPolygon());
if (intersection.getArea() == 0) {
continue;
}
NamedArea namedArea = new NamedArea();
OSMWithTags areaEntity = area.parent;
int cls = StreetEdge.CLASS_OTHERPATH;
cls |= getStreetClasses(areaEntity);
namedArea.setStreetClass(cls);
String id = "way (area) " + areaEntity.getId() + " (splitter linking)";
id = unique(id);
String name = getNameForWay(areaEntity, id);
namedArea.setName(name);
WayProperties wayData = wayPropertySet.getDataForWay(areaEntity);
Double safety = wayData.getSafetyFeatures().getFirst();
namedArea.setBicycleSafetyMultiplier(safety);
namedArea.setOriginalEdges(intersection);
StreetTraversalPermission permission = getPermissionsForEntity(areaEntity,
StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE);
namedArea.setPermission(permission);
edgeList.addArea(namedArea);
}
}
private void createSegments(OSMNode fromNode, OSMNode toNode,
IntersectionVertex startEndpoint, IntersectionVertex endEndpoint,
Collection<Area> areas, AreaEdgeList edgeList, Set<Edge> edges) {
List<Area> intersects = new ArrayList<Area>();
Coordinate[] coordinates = new Coordinate[] { startEndpoint.getCoordinate(),
endEndpoint.getCoordinate() };
GeometryFactory geometryFactory = GeometryUtils.getGeometryFactory();
LineString line = geometryFactory.createLineString(coordinates);
for (Area area : areas) {
MultiPolygon polygon = area.toJTSMultiPolygon();
Geometry intersection = polygon.intersection(line);
if (intersection.getLength() > 0.000001) {
intersects.add(area);
}
}
if (intersects.size() == 0) {
// apparently our intersection here was bogus
return;
}
// do we need to recurse?
if (intersects.size() == 1) {
Area area = intersects.get(0);
OSMWithTags areaEntity = area.parent;
StreetTraversalPermission areaPermissions = getPermissionsForEntity(areaEntity,
StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE);
float carSpeed = wayPropertySet.getCarSpeedForWay(areaEntity, false);
double length = distanceLibrary.distance(startEndpoint.getCoordinate(),
endEndpoint.getCoordinate());
int cls = StreetEdge.CLASS_OTHERPATH;
cls |= getStreetClasses(areaEntity);
String label = "way (area) " + areaEntity.getId() + " from "
+ startEndpoint.getLabel() + " to " + endEndpoint.getLabel();
label = unique(label);
String name = getNameForWay(areaEntity, label);
AreaEdge street = edgeFactory.createAreaEdge(fromNode, toNode, areaEntity,
startEndpoint, endEndpoint, line, name, length, areaPermissions, false,
carSpeed, edgeList);
street.setStreetClass(cls);
edges.add(street);
label = "way (area) " + areaEntity.getId() + " from " + endEndpoint.getLabel()
+ " to " + startEndpoint.getLabel();
label = unique(label);
name = getNameForWay(areaEntity, label);
AreaEdge backStreet = edgeFactory.createAreaEdge(toNode, fromNode, areaEntity,
endEndpoint, startEndpoint, (LineString) line.reverse(), name, length,
areaPermissions, true, carSpeed, edgeList);
backStreet.setStreetClass(cls);
edges.add(backStreet);
WayProperties wayData = wayPropertySet.getDataForWay(areaEntity);
applyWayProperties(street, backStreet, wayData, areaEntity);
} else {
// take the part that intersects with the start vertex
Coordinate startCoordinate = startEndpoint.getCoordinate();
Point startPoint = geometryFactory.createPoint(startCoordinate);
for (Area area : intersects) {
MultiPolygon polygon = area.toJTSMultiPolygon();
if (!(polygon.intersects(startPoint) || polygon.getBoundary().intersects(
startPoint)))
continue;
Geometry lineParts = line.intersection(polygon);
if (lineParts.getLength() > 0.000001) {
Coordinate edgeCoordinate = null;
// this is either a LineString or a MultiLineString (we hope)
if (lineParts instanceof MultiLineString) {
MultiLineString mls = (MultiLineString) lineParts;
boolean found = false;
for (int i = 0; i < mls.getNumGeometries(); ++i) {
LineString segment = (LineString) mls.getGeometryN(i);
if (found) {
edgeCoordinate = segment.getEndPoint().getCoordinate();
break;
}
if (segment.contains(startPoint)
|| segment.getBoundary().contains(startPoint)) {
found = true;
if (segment.getLength() > 0.000001) {
edgeCoordinate = segment.getEndPoint().getCoordinate();
break;
}
}
}
} else if (lineParts instanceof LineString) {
edgeCoordinate = ((LineString) lineParts).getEndPoint().getCoordinate();
} else {
continue;
}
IntersectionVertex newEndpoint = areaBoundaryVertexForCoordinate
.get(edgeCoordinate);
if (newEndpoint == null) {
newEndpoint = new IntersectionVertex(graph, "area splitter at "
+ edgeCoordinate, edgeCoordinate.x, edgeCoordinate.y);
areaBoundaryVertexForCoordinate.put(edgeCoordinate, newEndpoint);
}
createSegments(fromNode, toNode, startEndpoint, newEndpoint,
Arrays.asList(area), edgeList, edges);
createSegments(fromNode, toNode, newEndpoint, endEndpoint, intersects,
edgeList, edges);
break;
}
}
}
}
private void accumulateRingNodes(Ring ring, List<OSMNode> nodes, List<VLPoint> vertices) {
for (OSMNode node : ring.nodes) {
if (nodes.contains(node)) {
// hopefully, this only happens in order to
// close polygons
continue;
}
VLPoint point = new VLPoint(node.getLon(), node.getLat());
nodes.add(node);
vertices.add(point);
}
}
private void accumulateVisibilityPoints(List<OSMNode> nodes, VLPolygon polygon,
List<VLPoint> visibilityPoints, List<OSMNode> visibilityNodes, boolean hole) {
int n = polygon.vertices.size();
for (int i = 0; i < n; ++i) {
OSMNode curNode = nodes.get(i);
VLPoint cur = polygon.vertices.get(i);
VLPoint prev = polygon.vertices.get((i + n - 1) % n);
VLPoint next = polygon.vertices.get((i + 1) % n);
if (hole
|| (cur.x - prev.x) * (next.y - cur.y) - (cur.y - prev.y)
* (next.x - cur.x) > 0) {
// that math up there is a cross product to check
// if the point is concave. Note that the sign is reversed because
// visilibity is either ccw or latitude-major
if (!visibilityNodes.contains(curNode)) {
visibilityPoints.add(cur);
visibilityNodes.add(curNode);
}
}
}
}
private VLPolygon makeStandardizedVLPolygon(List<VLPoint> vertices, List<OSMNode> nodes,
boolean reversed) {
VLPolygon polygon = new VLPolygon(vertices);
if ((reversed && polygon.area() > 0) || (!reversed && polygon.area() < 0)) {
polygon.reverse();
// need to reverse nodes as well
reversePolygonOfOSMNodes(nodes);
}
if (!polygon.is_in_standard_form()) {
standardize(polygon.vertices, nodes);
}
return polygon;
}
private List<AreaGroup> groupAreas(List<Area> areas) {
DisjointSet<Area> groups = new DisjointSet<Area>();
HashMap<OSMNode, List<Area>> areasForNode = new HashMap<OSMNode, List<Area>>();
for (Area area : areas) {
for (Ring ring : area.outermostRings) {
for (Ring inner : ring.holes) {
for (OSMNode node : inner.nodes) {
MapUtils.addToMapList(areasForNode, node, area);
}
}
for (OSMNode node : ring.nodes) {
MapUtils.addToMapList(areasForNode, node, area);
}
}
}
// areas that can be joined must share nodes and levels
for (List<Area> nodeAreas : areasForNode.values()) {
for (Area area1 : nodeAreas) {
OSMLevel level1 = wayLevels.get(area1.parent);
for (Area area2 : nodeAreas) {
OSMLevel level2 = wayLevels.get(area2.parent);
if ((level1 == null && level2 == null)
|| (level1 != null && level1.equals(level2))) {
groups.union(area1, area2);
}
}
}
}
List<AreaGroup> out = new ArrayList<AreaGroup>();
for (Set<Area> areaSet : groups.sets()) {
try {
out.add(new AreaGroup(areaSet));
} catch (AreaGroup.RingConstructionException e) {
for (Area area : areaSet) {
LOG.debug("Failed to create merged area for "
+ area
+ ". This area might not be at fault; it might be one of the other areas in this list.");
out.add(new AreaGroup(Arrays.asList(area)));
}
}
}
return out;
}
private void standardize(ArrayList<VLPoint> vertices, List<OSMNode> nodes) {
// based on code from VisiLibity
int point_count = vertices.size();
if (point_count > 1) { // if more than one point in the polygon.
ArrayList<VLPoint> vertices_temp = new ArrayList<VLPoint>(point_count);
ArrayList<OSMNode> nodes_temp = new ArrayList<OSMNode>(point_count);
// Find index of lexicographically smallest point.
int index_of_smallest = 0;
for (int i = 1; i < point_count; i++)
if (vertices.get(i).compareTo(vertices.get(index_of_smallest)) < 0)
index_of_smallest = i;
// minor optimization for already-standardized polygons
if (index_of_smallest == 0)
return;
// Fill vertices_temp starting with lex. smallest.
for (int i = index_of_smallest; i < point_count; i++) {
vertices_temp.add(vertices.get(i));
nodes_temp.add(nodes.get(i));
}
for (int i = 0; i < index_of_smallest; i++) {
vertices_temp.add(vertices.get(i));
nodes_temp.add(nodes.get(i));
}
for (int i = 0; i < point_count; ++i) {
vertices.set(i, vertices_temp.get(i));
nodes.set(i, nodes_temp.get(i));
}
}
}
private boolean multipleAreasContain(long id) {
Set<OSMWay> areas = _areasForNode.get(id);
if (areas == null) {
return false;
}
return areas.size() > 1;
}
class ListedEdgesOnly implements SkipEdgeStrategy {
private Set<Edge> edges;
public ListedEdgesOnly(Set<Edge> edges) {
this.edges = edges;
}
@Override
public boolean shouldSkipEdge(Vertex origin, Vertex target, State current, Edge edge,
ShortestPathTree spt, RoutingRequest traverseOptions) {
return !edges.contains(edge);
}
}
/**
* Do an all-pairs shortest path search from a list of vertices over a specified set of edges, and retain only those edges which are actually
* used in some shortest path.
*
* @param startingVertices
* @param edges
*/
private void pruneAreaEdges(Collection<Vertex> startingVertices, Set<Edge> edges) {
if (edges.size() == 0)
return;
TraverseMode mode;
PlainStreetEdge firstEdge = (PlainStreetEdge) edges.iterator().next();
if (firstEdge.getPermission().allows(StreetTraversalPermission.PEDESTRIAN)) {
mode = TraverseMode.WALK;
} else if (firstEdge.getPermission().allows(StreetTraversalPermission.BICYCLE)) {
mode = TraverseMode.BICYCLE;
} else {
mode = TraverseMode.CAR;
}
RoutingRequest options = new RoutingRequest(mode);
GenericDijkstra search = new GenericDijkstra(options);
search.setSkipEdgeStrategy(new ListedEdgesOnly(edges));
Set<Edge> usedEdges = new HashSet<Edge>();
for (Vertex vertex : startingVertices) {
State state = new State(vertex, options);
ShortestPathTree spt = search.getShortestPathTree(state);
for (Vertex endVertex : startingVertices) {
GraphPath path = spt.getPath(endVertex, false);
if (path != null) {
for (Edge edge : path.edges) {
usedEdges.add(edge);
}
}
}
}
for (Edge edge : edges) {
if (!usedEdges.contains(edge)) {
edge.detach();
}
}
}
private void reversePolygonOfOSMNodes(List<OSMNode> nodes) {
for (int i = 1; i < (nodes.size() + 1) / 2; ++i) {
OSMNode tmp = nodes.get(i);
int opposite = nodes.size() - i;
nodes.set(i, nodes.get(opposite));
nodes.set(opposite, tmp);
}
}
private void buildBasicGraph() {
/* build the street segment graph from OSM ways */
long wayIndex = 0;
WAY: for (OSMWay way : _ways.values()) {
if (wayIndex % 10000 == 0)
LOG.debug("ways=" + wayIndex + "/" + _ways.size());
wayIndex++;
WayProperties wayData = wayPropertySet.getDataForWay(way);
setWayName(way);
StreetTraversalPermission permissions = getPermissionsForWay(way,
wayData.getPermission());
if (permissions.allowsNothing())
continue;
// handle duplicate nodes in OSM ways
// this is a workaround for crappy OSM data quality
ArrayList<Long> nodes = new ArrayList<Long>(way.getNodeRefs().size());
long last = -1;
double lastLat = -1, lastLon = -1;
String lastLevel = null;
for (long nodeId : way.getNodeRefs()) {
OSMNode node = _nodes.get(nodeId);
if (node == null)
continue WAY;
boolean levelsDiffer = false;
String level = node.getTag("level");
if (lastLevel == null) {
if (level != null) {
levelsDiffer = true;
}
} else {
if (!lastLevel.equals(level)) {
levelsDiffer = true;
}
}
if (nodeId != last
&& (node.getLat() != lastLat || node.getLon() != lastLon || levelsDiffer))
nodes.add(nodeId);
last = nodeId;
lastLon = node.getLon();
lastLat = node.getLat();
lastLevel = level;
}
IntersectionVertex startEndpoint = null, endEndpoint = null;
ArrayList<Coordinate> segmentCoordinates = new ArrayList<Coordinate>();
getLevelsForWay(way);
/*
* Traverse through all the nodes of this edge. For nodes which are not shared with any other edge, do not create endpoints -- just
* accumulate them for geometry and ele tags. For nodes which are shared, create endpoints and StreetVertex instances. One exception:
* if the next vertex also appears earlier in the way, we need to split the way, because otherwise we have a way that loops from a
* vertex to itself, which could cause issues with splitting.
*/
Long startNode = null;
// where the current edge should start
OSMNode osmStartNode = null;
for (int i = 0; i < nodes.size() - 1; i++) {
OSMNode segmentStartOSMNode = _nodes.get(nodes.get(i));
if (segmentStartOSMNode == null) {
continue;
}
Long endNode = nodes.get(i + 1);
if (osmStartNode == null) {
startNode = nodes.get(i);
osmStartNode = segmentStartOSMNode;
}
// where the current edge might end
OSMNode osmEndNode = _nodes.get(endNode);
if (osmStartNode == null || osmEndNode == null)
continue;
LineString geometry;
/*
* We split segments at intersections, self-intersections, and nodes with ele tags; the only processing we do on other nodes is to
* accumulate their geometry
*/
if (segmentCoordinates.size() == 0) {
segmentCoordinates.add(getCoordinate(osmStartNode));
}
if (intersectionNodes.containsKey(endNode) || i == nodes.size() - 2
|| nodes.subList(0, i).contains(nodes.get(i))
|| osmEndNode.hasTag("ele")) {
segmentCoordinates.add(getCoordinate(osmEndNode));
geometry = GeometryUtils.getGeometryFactory().createLineString(
segmentCoordinates.toArray(new Coordinate[0]));
segmentCoordinates.clear();
} else {
segmentCoordinates.add(getCoordinate(osmEndNode));
continue;
}
/* generate endpoints */
if (startEndpoint == null) { // first iteration on this way
// make or get a shared vertex for flat intersections,
// one vertex per level for multilevel nodes like elevators
startEndpoint = getVertexForOsmNode(osmStartNode, way);
String ele = segmentStartOSMNode.getTag("ele");
if (ele != null) {
Double elevation = ElevationUtils.parseEleTag(ele);
if (elevation != null) {
elevationData.put(startEndpoint, elevation);
}
}
} else { // subsequent iterations
startEndpoint = endEndpoint;
}
endEndpoint = getVertexForOsmNode(osmEndNode, way);
String ele = osmEndNode.getTag("ele");
if (ele != null) {
Double elevation = ElevationUtils.parseEleTag(ele);
if (elevation != null) {
elevationData.put(endEndpoint, elevation);
}
}
P2<PlainStreetEdge> streets = getEdgesForStreet(startEndpoint, endEndpoint,
way, i, osmStartNode.getId(), osmEndNode.getId(), permissions, geometry);
PlainStreetEdge street = streets.getFirst();
PlainStreetEdge backStreet = streets.getSecond();
applyWayProperties(street, backStreet, wayData, way);
applyEdgesToTurnRestrictions(way, startNode, endNode, street, backStreet);
startNode = endNode;
osmStartNode = _nodes.get(startNode);
}
} // END loop over OSM ways
}
private void applyWayProperties(PlainStreetEdge street, PlainStreetEdge backStreet,
WayProperties wayData, OSMWithTags way) {
Set<Alert> note = wayPropertySet.getNoteForWay(way);
Set<Alert> wheelchairNote = getWheelchairNotes(way);
boolean noThruTraffic = way.isThroughTrafficExplicitlyDisallowed();
if (street != null) {
double safety = wayData.getSafetyFeatures().getFirst();
street.setBicycleSafetyEffectiveLength(street.getLength() * safety);
if (safety < bestBikeSafety) {
bestBikeSafety = safety;
}
if (note != null) {
street.setNote(note);
}
if (wheelchairNote != null) {
street.setWheelchairNote(wheelchairNote);
}
street.setNoThruTraffic(noThruTraffic);
}
if (backStreet != null) {
double safety = wayData.getSafetyFeatures().getSecond();
if (safety < bestBikeSafety) {
bestBikeSafety = safety;
}
backStreet.setBicycleSafetyEffectiveLength(backStreet.getLength() * safety);
if (note != null) {
backStreet.setNote(note);
}
if (wheelchairNote != null) {
backStreet.setWheelchairNote(wheelchairNote);
}
backStreet.setNoThruTraffic(noThruTraffic);
}
}
private void setWayName(OSMWithTags way) {
if (!way.hasTag("name")) {
String creativeName = wayPropertySet.getCreativeNameForWay(way);
if (creativeName != null) {
way.addTag("otp:gen_name", creativeName);
}
}
}
private void buildElevatorEdges(Graph graph) {
/* build elevator edges */
for (Long nodeId : multiLevelNodes.keySet()) {
OSMNode node = _nodes.get(nodeId);
// this allows skipping levels, e.g., an elevator that stops
// at floor 0, 2, 3, and 5.
// Converting to an Array allows us to
// subscript it so we can loop over it in twos. Assumedly, it will stay
// sorted when we convert it to an Array.
// The objects are Integers, but toArray returns Object[]
HashMap<OSMLevel, IntersectionVertex> vertices = multiLevelNodes.get(nodeId);
/*
* first, build FreeEdges to disconnect from the graph, GenericVertices to serve as attachment points, and ElevatorBoard and
* ElevatorAlight edges to connect future ElevatorHop edges to. After this iteration, graph will look like (side view): +==+~~X
*
* +==+~~X
*
* +==+~~X
*
* + GenericVertex, X EndpointVertex, ~~ FreeEdge, == ElevatorBoardEdge/ElevatorAlightEdge Another loop will fill in the
* ElevatorHopEdges.
*/
OSMLevel[] levels = vertices.keySet().toArray(new OSMLevel[0]);
Arrays.sort(levels);
ArrayList<Vertex> onboardVertices = new ArrayList<Vertex>();
for (OSMLevel level : levels) {
// get the node to build the elevator out from
IntersectionVertex sourceVertex = vertices.get(level);
String sourceVertexLabel = sourceVertex.getLabel();
String levelName = level.longName;
ElevatorOffboardVertex offboardVertex = new ElevatorOffboardVertex(graph,
sourceVertexLabel + "_offboard", sourceVertex.getX(),
sourceVertex.getY(), levelName);
new FreeEdge(sourceVertex, offboardVertex);
new FreeEdge(offboardVertex, sourceVertex);
ElevatorOnboardVertex onboardVertex = new ElevatorOnboardVertex(graph,
sourceVertexLabel + "_onboard", sourceVertex.getX(),
sourceVertex.getY(), levelName);
new ElevatorBoardEdge(offboardVertex, onboardVertex);
new ElevatorAlightEdge(onboardVertex, offboardVertex, level.longName);
// accumulate onboard vertices to so they can be connected by hop edges later
onboardVertices.add(onboardVertex);
}
// -1 because we loop over onboardVertices two at a time
for (Integer i = 0, vSize = onboardVertices.size() - 1; i < vSize; i++) {
Vertex from = onboardVertices.get(i);
Vertex to = onboardVertices.get(i + 1);
// default permissions: pedestrian, wheelchair, and bicycle
boolean wheelchairAccessible = true;
StreetTraversalPermission permission = StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE;
// check for bicycle=no, otherwise assume it's OK to take a bike
if (node.isTagFalse("bicycle")) {
permission = StreetTraversalPermission.PEDESTRIAN;
}
// check for wheelchair=no
if (node.isTagFalse("wheelchair")) {
wheelchairAccessible = false;
}
// The narrative won't be strictly correct, as it will show the elevator as part
// of the cycling leg, but I think most cyclists will figure out that they
// should really dismount.
ElevatorHopEdge foreEdge = new ElevatorHopEdge(from, to, permission);
ElevatorHopEdge backEdge = new ElevatorHopEdge(to, from, permission);
foreEdge.wheelchairAccessible = wheelchairAccessible;
backEdge.wheelchairAccessible = wheelchairAccessible;
}
} // END elevator edge loop
}
private void applyEdgesToTurnRestrictions(OSMWay way, long startNode, long endNode,
PlainStreetEdge street, PlainStreetEdge backStreet) {
/* Check if there are turn restrictions starting on this segment */
List<TurnRestrictionTag> restrictionTags = turnRestrictionsByFromWay.get(way.getId());
if (restrictionTags != null) {
for (TurnRestrictionTag tag : restrictionTags) {
if (tag.via == startNode) {
tag.possibleFrom.add(backStreet);
} else if (tag.via == endNode) {
tag.possibleFrom.add(street);
}
}
}
restrictionTags = turnRestrictionsByToWay.get(way.getId());
if (restrictionTags != null) {
for (TurnRestrictionTag tag : restrictionTags) {
if (tag.via == startNode) {
tag.possibleTo.add(street);
} else if (tag.via == endNode) {
tag.possibleTo.add(backStreet);
}
}
}
}
private void getLevelsForWay(OSMWithTags way) {
/* Determine OSM level for each way, if it was not already set */
if (!wayLevels.containsKey(way)) {
// if this way is not a key in the wayLevels map, a level map was not
// already applied in processRelations
/* try to find a level name in tags */
String levelName = null;
OSMLevel level = OSMLevel.DEFAULT;
if (way.hasTag("level")) { // TODO: floating-point levels &c.
levelName = way.getTag("level");
level = OSMLevel.fromString(levelName, OSMLevel.Source.LEVEL_TAG, noZeroLevels);
} else if (way.hasTag("layer")) {
levelName = way.getTag("layer");
level = OSMLevel.fromString(levelName, OSMLevel.Source.LAYER_TAG, noZeroLevels);
}
if (level == null || (!level.reliable)) {
LOG.warn(graph.addBuilderAnnotation(new LevelAmbiguous(levelName, way.getId())));
level = OSMLevel.DEFAULT;
}
wayLevels.put(way, level);
}
}
private void initIntersectionNodes() {
Set<Long> possibleIntersectionNodes = new HashSet<Long>();
for (OSMWay way : _ways.values()) {
List<Long> nodes = way.getNodeRefs();
for (long node : nodes) {
if (possibleIntersectionNodes.contains(node)) {
intersectionNodes.put(node, null);
} else {
possibleIntersectionNodes.add(node);
}
}
}
}
private Set<Alert> getWheelchairNotes(OSMWithTags way) {
Map<String, String> tags = way.getTagsByPrefix("wheelchair:description");
if (tags == null) {
return null;
}
Set<Alert> alerts = new HashSet<Alert>();
Alert alert = new Alert();
alerts.add(alert);
for (Map.Entry<String, String> entry : tags.entrySet()) {
String k = entry.getKey();
String v = entry.getValue();
if (k.equals("wheelchair:description")) {
// no language, assume default from TranslatedString
alert.alertHeaderText = new TranslatedString(v);
} else {
String lang = k.substring("wheelchair:description:".length());
alert.alertHeaderText = new TranslatedString(lang, v);
}
}
return alerts;
}
/**
* The safest bike lane should have a safety weight no lower than the time weight of a flat street. This method divides the safety lengths by
* the length ratio of the safest street, ensuring this property.
*
* @param graph
*/
private void applyBikeSafetyFactor(Graph graph) {
LOG.info(graph.addBuilderAnnotation(new Graphwide(
"Multiplying all bike safety values by " + (1 / bestBikeSafety))));
HashSet<Edge> seenEdges = new HashSet<Edge>();
HashSet<AreaEdgeList> seenAreas = new HashSet<AreaEdgeList>();
for (Vertex vertex : graph.getVertices()) {
for (Edge e : vertex.getOutgoing()) {
if (e instanceof AreaEdge) {
AreaEdgeList areaEdgeList = ((AreaEdge) e).getArea();
if (seenAreas.contains(areaEdgeList))
continue;
seenAreas.add(areaEdgeList);
for (NamedArea area : areaEdgeList.getAreas()) {
area.setBicycleSafetyMultiplier(area.getBicycleSafetyMultiplier()
/ bestBikeSafety);
}
}
if (!(e instanceof PlainStreetEdge)) {
continue;
}
PlainStreetEdge pse = (PlainStreetEdge) e;
if (!seenEdges.contains(e)) {
seenEdges.add(e);
pse.setBicycleSafetyEffectiveLength(pse.getBicycleSafetyEffectiveLength()
/ bestBikeSafety);
}
}
for (Edge e : vertex.getIncoming()) {
if (!(e instanceof PlainStreetEdge)) {
continue;
}
PlainStreetEdge pse = (PlainStreetEdge) e;
if (!seenEdges.contains(e)) {
seenEdges.add(e);
pse.setBicycleSafetyEffectiveLength(pse.getBicycleSafetyEffectiveLength()
/ bestBikeSafety);
}
}
}
}
private Coordinate getCoordinate(OSMNode osmNode) {
return new Coordinate(osmNode.getLon(), osmNode.getLat());
}
public void addNode(OSMNode node) {
if (node.isTag("amenity", "bicycle_rental")) {
_bikeRentalNodes.add(node);
return;
}
if (!(_nodesWithNeighbors.contains(node.getId()) || _areaNodes.contains(node.getId())))
return;
if (_nodes.containsKey(node.getId()))
return;
_nodes.put(node.getId(), node);
if (_nodes.size() % 100000 == 0)
LOG.debug("nodes=" + _nodes.size());
}
public void addWay(OSMWay way) {
/* only add ways once */
long wayId = way.getId();
if (_ways.containsKey(wayId) || _areaWaysById.containsKey(wayId))
return;
if (_areaWayIds.contains(wayId)) {
_areaWaysById.put(wayId, way);
}
/* filter out ways that are not relevant for routing */
if (!isWayRoutable(way)) {
return;
}
if (way.isTag("area", "yes") && way.getNodeRefs().size() > 2) {
// this is an area that's a simple polygon. So we can just add it straight
// to the areas, if it's not part of a relation.
if (!_areaWayIds.contains(wayId)) {
_singleWayAreas.add(way);
_areaWaysById.put(wayId, way);
_areaWayIds.add(wayId);
for (Long node : way.getNodeRefs()) {
MapUtils.addToMapSet(_areasForNode, node, way);
}
getLevelsForWay(way);
}
return;
}
_ways.put(wayId, way);
if (_ways.size() % 10000 == 0)
LOG.debug("ways=" + _ways.size());
}
/**
* Determine whether any mode can ever traverse the given way.
* If not, we can safely leave the way out of the OTP graph without affecting routing.
* Potentially routable ways are those that have the tags :
* highway=*
* public_transport=platform
* railway=platform
* But not conveyers, proposed highways/roads, and raceways (as well as ways where all
* access is specifically forbidden to the public).
*/
private boolean isWayRoutable(OSMWithTags way) {
if (!isOsmEntityRoutable(way))
return false;
String highway = way.getTag("highway");
if (highway != null && (highway.equals("conveyer") || highway.equals("proposed") ||
highway.equals("raceway")))
return false;
if (way.isGeneralAccessDenied()) {
// There are exceptions.
return (way.isMotorcarExplicitlyAllowed() || way.isBicycleExplicitlyAllowed() ||
way.isPedestrianExplicitlyAllowed());
}
return true;
}
public void addRelation(OSMRelation relation) {
if (_relations.containsKey(relation.getId()))
return;
if (relation.isTag("type", "multipolygon") && isOsmEntityRoutable(relation)) {
// OSM MultiPolygons are ferociously complicated, and in fact cannot be processed
// without reference to the ways that compose them. Accordingly, we will merely
// mark the ways for preservation here, and deal with the details once we have
// the ways loaded.
if (!isWayRoutable(relation)) {
return;
}
for (OSMRelationMember member : relation.getMembers()) {
_areaWayIds.add(member.getRef());
}
getLevelsForWay(relation);
} else if (!(relation.isTag("type", "restriction"))
&& !(relation.isTag("type", "route") && relation.isTag("route", "road"))
&& !(relation.isTag("type", "multipolygon") && isOsmEntityRoutable(relation))
&& !(relation.isTag("type", "level_map"))) {
return;
}
_relations.put(relation.getId(), relation);
if (_relations.size() % 100 == 0)
LOG.debug("relations=" + _relations.size());
}
/**
* Determines whether this OSM way is considered routable.
* The majority of routable ways are those with a highway= tag (which includes everything
* from motorways to hiking trails). Anything with a public_transport=platform or
* railway=platform tag is also considered routable even if it doesn't have a highway tag.
* Platforms are however filtered out if they are marked usage=tourism. This prevents
* miniature tourist railways like the one in Portland's Zoo from receiving a better score
* and pulling search endpoints away from real transit stops.
*/
private boolean isOsmEntityRoutable(OSMWithTags osmEntity) {
if (osmEntity.hasTag("highway"))
return true;
if (osmEntity.isTag("public_transport", "platform") ||
osmEntity.isTag("railway", "platform")) {
return ! ("tourism".equals(osmEntity.getTag("usage")));
}
return false;
}
private String getNodeLabel(OSMNode node) {
return String.format(nodeLabelFormat, node.getId());
}
private String getLevelNodeLabel(OSMNode node, OSMLevel level) {
return String.format(levelnodeLabelFormat, node.getId(), level.shortName);
}
public void secondPhase() {
// This copies relevant tags to the ways (highway=*) where it doesn't exist, so that
// the way purging keeps the needed way around.
// Multipolygons may be processed more than once, which may be needed since
// some member might be in different files for the same multipolygon.
// NOTE (AMB): this purging phase may not be necessary if highway tags are not
// copied over from multipolygon relations. Perhaps we can get by with
// only 2 steps -- ways+relations, followed by used nodes.
// Ways can be tag-filtered in phase 1.
markNodesForKeeping(_ways.values(), _nodesWithNeighbors);
markNodesForKeeping(_areaWaysById.values(), _areaNodes);
}
/**
* After all relations, ways, and nodes are loaded, handle areas.
*/
public void nodesLoaded() {
processMultipolygonRelations();
AREA: for (OSMWay way : _singleWayAreas) {
if (_processedAreas.contains(way)) {
continue;
}
for (Long nodeRef : way.getNodeRefs()) {
if (!_nodes.containsKey(nodeRef)) {
continue AREA;
}
}
try {
StreetTraversalPermission areaPermissions = getPermissionsForEntity(way,
StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE);
if (areaPermissions == StreetTraversalPermission.NONE)
continue;
_areas.add(new Area(way, Arrays.asList(way), Collections.<OSMWay> emptyList()));
} catch (Area.AreaConstructionException e) {
// this area cannot be constructed, but we already have all the
// necessary nodes to construct it. So, something must be wrong with
// the area; we'll mark it as processed so that we don't retry.
}
_processedAreas.add(way);
}
}
private void markNodesForKeeping(Collection<OSMWay> osmWays, Set<Long> nodeSet) {
for (Iterator<OSMWay> it = osmWays.iterator(); it.hasNext();) {
OSMWay way = it.next();
// Since the way is kept, update nodes-with-neighbors
List<Long> nodes = way.getNodeRefs();
if (nodes.size() > 1) {
nodeSet.addAll(nodes);
}
}
}
/**
* Copies useful metadata from multipolygon relations to the relevant ways, or to the area
* map. This is done at a different time than processRelations(), so that way purging
* doesn't remove the used ways.
*/
private void processMultipolygonRelations() {
RELATION: for (OSMRelation relation : _relations.values()) {
if (_processedAreas.contains(relation)) {
continue;
}
if (!(relation.isTag("type", "multipolygon") && isOsmEntityRoutable(relation))) {
continue;
}
// Area multipolygons -- pedestrian plazas
ArrayList<OSMWay> innerWays = new ArrayList<OSMWay>();
ArrayList<OSMWay> outerWays = new ArrayList<OSMWay>();
for (OSMRelationMember member : relation.getMembers()) {
String role = member.getRole();
OSMWay way = _areaWaysById.get(member.getRef());
if (way == null) {
// relation includes way which does not exist in the data. Skip.
continue RELATION;
}
for (Long nodeId : way.getNodeRefs()) {
if (!_nodes.containsKey(nodeId)) {
// this area is missing some nodes, perhaps because it is on
// the edge of the region, so we will simply not route on it.
continue RELATION;
}
MapUtils.addToMapSet(_areasForNode, nodeId, way);
}
if (role.equals("inner")) {
innerWays.add(way);
} else if (role.equals("outer")) {
outerWays.add(way);
} else {
LOG.warn("Unexpected role " + role + " in multipolygon");
}
}
_processedAreas.add(relation);
Area area;
try {
StreetTraversalPermission areaPermissions = getPermissionsForEntity(relation,
StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE);
if (areaPermissions == StreetTraversalPermission.NONE)
continue;
area = new Area(relation, outerWays, innerWays);
} catch (Area.AreaConstructionException e) {
continue;
}
_areas.add(area);
for (OSMRelationMember member : relation.getMembers()) {
// multipolygons for attribute mapping
if (!("way".equals(member.getType()) && _ways.containsKey(member.getRef()))) {
continue;
}
OSMWithTags way = _ways.get(member.getRef());
if (way == null) {
continue;
}
String[] relationCopyTags = { "highway", "name", "ref" };
for (String tag : relationCopyTags) {
if (relation.hasTag(tag) && !way.hasTag(tag)) {
way.addTag(tag, relation.getTag(tag));
}
}
if (relation.isTag("railway", "platform") && !way.hasTag("railway")) {
way.addTag("railway", "platform");
}
if (relation.isTag("public_transport", "platform")
&& !way.hasTag("public_transport")) {
way.addTag("public_transport", "platform");
}
}
}
}
/**
* Copies useful metadata from relations to the relevant ways/nodes.
*/
private void processRelations() {
LOG.debug("Processing relations...");
for (OSMRelation relation : _relations.values()) {
if (relation.isTag("type", "restriction")) {
processRestriction(relation);
} else if (relation.isTag("type", "level_map")) {
processLevelMap(relation);
} else if (relation.isTag("type", "route")) {
processRoad(relation);
}
// multipolygons will be further processed in secondPhase()
}
}
/**
* Store turn restrictions.
*
* @param relation
*/
private void processRestriction(OSMRelation relation) {
long from = -1, to = -1, via = -1;
for (OSMRelationMember member : relation.getMembers()) {
String role = member.getRole();
if (role.equals("from")) {
from = member.getRef();
} else if (role.equals("to")) {
to = member.getRef();
} else if (role.equals("via")) {
via = member.getRef();
}
}
if (from == -1 || to == -1 || via == -1) {
LOG.warn(graph.addBuilderAnnotation(new TurnRestrictionBad(relation.getId())));
return;
}
TraverseModeSet modes = new TraverseModeSet(TraverseMode.BICYCLE, TraverseMode.CAR,
TraverseMode.CUSTOM_MOTOR_VEHICLE);
String exceptModes = relation.getTag("except");
if (exceptModes != null) {
for (String m : exceptModes.split(";")) {
if (m.equals("motorcar")) {
modes.setDriving(false);
} else if (m.equals("bicycle")) {
modes.setBicycle(false);
LOG.debug(graph
.addBuilderAnnotation(new TurnRestrictionException(via, from)));
}
}
}
TurnRestrictionTag tag;
if (relation.isTag("restriction", "no_right_turn")) {
tag = new TurnRestrictionTag(via, TurnRestrictionType.NO_TURN, Direction.RIGHT);
} else if (relation.isTag("restriction", "no_left_turn")) {
tag = new TurnRestrictionTag(via, TurnRestrictionType.NO_TURN, Direction.LEFT);
} else if (relation.isTag("restriction", "no_straight_on")) {
tag = new TurnRestrictionTag(via, TurnRestrictionType.NO_TURN, Direction.STRAIGHT);
} else if (relation.isTag("restriction", "no_u_turn")) {
tag = new TurnRestrictionTag(via, TurnRestrictionType.NO_TURN, Direction.U);
} else if (relation.isTag("restriction", "only_straight_on")) {
tag = new TurnRestrictionTag(via, TurnRestrictionType.ONLY_TURN, Direction.STRAIGHT);
} else if (relation.isTag("restriction", "only_right_turn")) {
tag = new TurnRestrictionTag(via, TurnRestrictionType.ONLY_TURN, Direction.RIGHT);
} else if (relation.isTag("restriction", "only_left_turn")) {
tag = new TurnRestrictionTag(via, TurnRestrictionType.ONLY_TURN, Direction.LEFT);
} else if (relation.isTag("restriction", "only_u_turn")) {
tag = new TurnRestrictionTag(via, TurnRestrictionType.ONLY_TURN, Direction.U);
} else {
LOG.warn(graph.addBuilderAnnotation(new TurnRestrictionUnknown(relation
.getTag("restriction"))));
return;
}
tag.modes = modes.clone();
// set the time periods for this restriction, if applicable
if (relation.hasTag("day_on") && relation.hasTag("day_off")
&& relation.hasTag("hour_on") && relation.hasTag("hour_off")) {
try {
tag.time = RepeatingTimePeriod.parseFromOsmTurnRestriction(
relation.getTag("day_on"), relation.getTag("day_off"),
relation.getTag("hour_on"), relation.getTag("hour_off"));
} catch (NumberFormatException e) {
LOG.info("Unparseable turn restriction: " + relation.getId());
}
}
MapUtils.addToMapList(turnRestrictionsByFromWay, from, tag);
MapUtils.addToMapList(turnRestrictionsByToWay, to, tag);
}
/**
* Process an OSM level map.
*
* @param relation
*/
private void processLevelMap(OSMRelation relation) {
Map<String, OSMLevel> levels = OSMLevel.mapFromSpecList(relation.getTag("levels"),
Source.LEVEL_MAP, true);
for (OSMRelationMember member : relation.getMembers()) {
if ("way".equals(member.getType()) && _ways.containsKey(member.getRef())) {
OSMWay way = _ways.get(member.getRef());
if (way != null) {
String role = member.getRole();
// if the level map relation has a role:xyz tag, this way is something
// more complicated than a single level (e.g. ramp/stairway).
if (!relation.hasTag("role:" + role)) {
if (levels.containsKey(role)) {
wayLevels.put(way, levels.get(role));
} else {
LOG.warn(member.getRef() + " has undefined level " + role);
}
}
}
}
}
}
/*
* Handle route=road relations.
*
* @param relation
*/
private void processRoad(OSMRelation relation) {
for (OSMRelationMember member : relation.getMembers()) {
if (!("way".equals(member.getType()) && _ways.containsKey(member.getRef()))) {
continue;
}
OSMWithTags way = _ways.get(member.getRef());
if (way == null) {
continue;
}
if (relation.hasTag("name")) {
if (way.hasTag("otp:route_name")) {
way.addTag(
"otp:route_name",
addUniqueName(way.getTag("otp:route_name"), relation.getTag("name")));
} else {
way.addTag(new OSMTag("otp:route_name", relation.getTag("name")));
}
}
if (relation.hasTag("ref")) {
if (way.hasTag("otp:route_ref")) {
way.addTag("otp:route_ref",
addUniqueName(way.getTag("otp:route_ref"), relation.getTag("ref")));
} else {
way.addTag(new OSMTag("otp:route_ref", relation.getTag("ref")));
}
}
}
}
private String addUniqueName(String routes, String name) {
String[] names = routes.split(", ");
for (String existing : names) {
if (existing.equals(name)) {
return routes;
}
}
return routes + ", " + name;
}
/**
* Returns the length of the geometry in meters.
*
* @param geometry
* @return
*/
private double getGeometryLengthMeters(Geometry geometry) {
Coordinate[] coordinates = geometry.getCoordinates();
double d = 0;
for (int i = 1; i < coordinates.length; ++i) {
d += distanceLibrary.distance(coordinates[i - 1], coordinates[i]);
}
return d;
}
/**
* Handle oneway streets, cycleways, and other per-mode and universal access controls. See http://wiki.openstreetmap.org/wiki/Bicycle for
* various scenarios, along with http://wiki.openstreetmap.org/wiki/OSM_tags_for_routing#Oneway.
*
* @param end
* @param start
*/
private P2<PlainStreetEdge> getEdgesForStreet(IntersectionVertex start,
IntersectionVertex end, OSMWay way, int index, long startNode, long endNode,
StreetTraversalPermission permissions, LineString geometry) {
// No point in returning edges that can't be traversed by anyone.
if (permissions.allowsNothing()) {
return new P2<PlainStreetEdge>(null, null);
}
LineString backGeometry = (LineString) geometry.reverse();
PlainStreetEdge street = null, backStreet = null;
double length = this.getGeometryLengthMeters(geometry);
P2<StreetTraversalPermission> permissionPair = getPermissions(permissions, way);
StreetTraversalPermission permissionsFront = permissionPair.getFirst();
StreetTraversalPermission permissionsBack = permissionPair.getSecond();
if (permissionsFront.allowsAnything()) {
street = getEdgeForStreet(start, end, way, index, startNode, endNode, length,
permissionsFront, geometry, false);
}
if (permissionsBack.allowsAnything()) {
backStreet = getEdgeForStreet(end, start, way, index, endNode, startNode, length,
permissionsBack, backGeometry, true);
}
/* mark edges that are on roundabouts */
if (way.isRoundabout()) {
if (street != null)
street.setRoundabout(true);
if (backStreet != null)
backStreet.setRoundabout(true);
}
return new P2<PlainStreetEdge>(street, backStreet);
}
/**
* Check OSM tags for various one-way and one-way-by-mode tags and return a pair of permissions for travel along and against the way.
*/
private P2<StreetTraversalPermission> getPermissions(StreetTraversalPermission permissions,
OSMWay way) {
StreetTraversalPermission permissionsFront = permissions;
StreetTraversalPermission permissionsBack = permissions;
// Check driving direction restrictions.
if (way.isOneWayForwardDriving() || way.isRoundabout()) {
permissionsBack = permissionsBack
.remove(StreetTraversalPermission.BICYCLE_AND_DRIVING);
}
if (way.isOneWayReverseDriving()) {
permissionsFront = permissionsFront
.remove(StreetTraversalPermission.BICYCLE_AND_DRIVING);
}
// Check bike direction restrictions.
if (way.isOneWayForwardBicycle()) {
permissionsBack = permissionsBack.remove(StreetTraversalPermission.BICYCLE);
}
if (way.isOneWayReverseBicycle()) {
permissionsFront = permissionsFront.remove(StreetTraversalPermission.BICYCLE);
}
// TODO(flamholz): figure out what this is for.
String oneWayBicycle = way.getTag("oneway:bicycle");
if (OSMWithTags.isFalse(oneWayBicycle) || way.isTagTrue("bicycle:backwards")) {
if (permissions.allows(StreetTraversalPermission.BICYCLE)) {
permissionsFront = permissionsFront.add(StreetTraversalPermission.BICYCLE);
permissionsBack = permissionsBack.add(StreetTraversalPermission.BICYCLE);
}
}
if (way.isOpposableCycleway()) {
permissionsBack = permissionsBack.add(StreetTraversalPermission.BICYCLE);
}
return new P2<StreetTraversalPermission>(permissionsFront, permissionsBack);
}
private PlainStreetEdge getEdgeForStreet(IntersectionVertex start, IntersectionVertex end,
OSMWay way, int index, long startNode, long endNode, double length,
StreetTraversalPermission permissions, LineString geometry, boolean back) {
String label = "way " + way.getId() + " from " + index;
label = unique(label);
String name = getNameForWay(way, label);
// consider the elevation gain of stairs, roughly
boolean steps = way.isSteps();
if (steps) {
length *= 2;
}
float carSpeed = wayPropertySet.getCarSpeedForWay(way, back);
PlainStreetEdge street = edgeFactory.createEdge(_nodes.get(startNode),
_nodes.get(endNode), way, start, end, geometry, name, length, permissions,
back, carSpeed);
String highway = way.getTag("highway");
int cls;
if ("crossing".equals(highway) && !way.isTag("bicycle", "designated")) {
cls = StreetEdge.CLASS_CROSSING;
} else if ("footway".equals(highway) && way.isTag("footway", "crossing")
&& !way.isTag("bicycle", "designated")) {
cls = StreetEdge.CLASS_CROSSING;
} else if ("residential".equals(highway) || "tertiary".equals(highway)
|| "secondary".equals(highway) || "secondary_link".equals(highway)
|| "primary".equals(highway) || "primary_link".equals(highway)
|| "trunk".equals(highway) || "trunk_link".equals(highway)) {
cls = StreetEdge.CLASS_STREET;
} else {
cls = StreetEdge.CLASS_OTHERPATH;
}
cls |= getStreetClasses(way);
street.setStreetClass(cls);
if (!way.hasTag("name") && !way.hasTag("ref")) {
street.setHasBogusName(true);
}
street.setStairs(steps);
if (way.isTagTrue("toll") || way.isTagTrue("toll:motorcar"))
street.setToll(true);
else
street.setToll(false);
/* TODO: This should probably generalized somehow? */
if (way.isTagFalse("wheelchair") || (steps && !way.isTagTrue("wheelchair"))) {
street.setWheelchairAccessible(false);
}
street.setSlopeOverride(wayPropertySet.getSlopeOverride(way));
// < 0.04: account for
if (carSpeed < 0.04) {
LOG.warn(graph.addBuilderAnnotation(new StreetCarSpeedZero(way.getId())));
}
if (customNamer != null) {
customNamer.nameWithEdge(way, street);
}
return street;
}
private int getStreetClasses(OSMWithTags way) {
int link = 0;
String highway = way.getTag("highway");
if (highway != null && highway.endsWith(("_link"))) {
link = StreetEdge.CLASS_LINK;
}
return getPlatformClass(way) | link;
}
private int getPlatformClass(OSMWithTags way) {
String highway = way.getTag("highway");
if ("platform".equals(way.getTag("railway"))) {
return StreetEdge.CLASS_TRAIN_PLATFORM;
}
if ("platform".equals(highway) || "platform".equals(way.getTag("public_transport"))) {
if (way.isTagTrue("train") || way.isTagTrue("subway") || way.isTagTrue("tram")
|| way.isTagTrue("monorail")) {
return StreetEdge.CLASS_TRAIN_PLATFORM;
}
return StreetEdge.CLASS_OTHER_PLATFORM;
}
return 0;
}
private String getNameForWay(OSMWithTags way, String id) {
String name = way.getAssumedName();
if (customNamer != null) {
name = customNamer.name(way, name);
}
if (name == null) {
name = id;
}
return name;
}
private StreetTraversalPermission getPermissionsForEntity(OSMWithTags entity,
StreetTraversalPermission def) {
StreetTraversalPermission permission = null;
/*
* Only a few tags are examined here, because we only care about modes supported by OTP (wheelchairs are not of concern here)
*
* Only a few values are checked for, all other values are presumed to be permissive (=> This may not be perfect, but is closer to
* reality, since most people don't follow the rules perfectly ;-)
*/
if (entity.isGeneralAccessDenied()) {
// this can actually be overridden
permission = StreetTraversalPermission.NONE;
if (entity.isMotorcarExplicitlyAllowed()) {
permission = permission.add(StreetTraversalPermission.ALL_DRIVING);
}
if (entity.isBicycleExplicitlyAllowed()) {
permission = permission.add(StreetTraversalPermission.BICYCLE);
}
if (entity.isPedestrianExplicitlyAllowed()) {
permission = permission.add(StreetTraversalPermission.PEDESTRIAN);
}
} else {
permission = def;
}
if (entity.isMotorcarExplicitlyDenied()) {
permission = permission.remove(StreetTraversalPermission.ALL_DRIVING);
} else if (entity.hasTag("motorcar")) {
permission = permission.add(StreetTraversalPermission.ALL_DRIVING);
}
if (entity.isBicycleExplicitlyDenied()) {
permission = permission.remove(StreetTraversalPermission.BICYCLE);
} else if (entity.hasTag("bicycle")) {
permission = permission.add(StreetTraversalPermission.BICYCLE);
}
if (entity.isPedestrianExplicitlyDenied()) {
permission = permission.remove(StreetTraversalPermission.PEDESTRIAN);
} else if (entity.hasTag("foot")) {
permission = permission.add(StreetTraversalPermission.PEDESTRIAN);
}
if (entity.isUnderConstruction()) {
permission = StreetTraversalPermission.NONE;
}
if (permission == null)
return def;
return permission;
}
/**
* Computes permissions for an OSMWay.
*
* @param way
* @param def
* @return
*/
private StreetTraversalPermission getPermissionsForWay(OSMWay way,
StreetTraversalPermission def) {
StreetTraversalPermission permissions = getPermissionsForEntity(way, def);
/*
* pedestrian rules: everything is two-way (assuming pedestrians are allowed at all) bicycle rules: default: permissions;
*
* cycleway=dismount means walk your bike -- the engine will automatically try walking bikes any time it is forbidden to ride them, so the
* only thing to do here is to remove bike permissions
*
* oneway=... sets permissions for cars and bikes oneway:bicycle overwrites these permissions for bikes only
*
* now, cycleway=opposite_lane, opposite, opposite_track can allow once oneway has been set by oneway:bicycle, but should give a warning
* if it conflicts with oneway:bicycle
*
* bicycle:backward=yes works like oneway:bicycle=no bicycle:backwards=no works like oneway:bicycle=yes
*/
// Compute pedestrian permissions.
if (way.isPedestrianExplicitlyAllowed()) {
permissions = permissions.add(StreetTraversalPermission.PEDESTRIAN);
} else if (way.isPedestrianExplicitlyDenied()) {
permissions = permissions.remove(StreetTraversalPermission.PEDESTRIAN);
}
// Compute bike permissions, check consistency.
boolean forceBikes = false;
if (way.isBicycleExplicitlyAllowed()) {
permissions = permissions.add(StreetTraversalPermission.BICYCLE);
forceBikes = true;
}
if (way.isBicycleDismountForced()) {
permissions = permissions.remove(StreetTraversalPermission.BICYCLE);
if (forceBikes) {
LOG.warn(graph.addBuilderAnnotation(new ConflictingBikeTags(way.getId())));
}
}
return permissions;
}
/**
* Record the level of the way for this node, e.g. if the way is at level 5, mark that this node is active at level 5.
*
* @param the way that has the level
* @param the node to record for
* @author mattwigway
*/
private IntersectionVertex recordLevel(OSMNode node, OSMWithTags way) {
OSMLevel level = wayLevels.get(way);
HashMap<OSMLevel, IntersectionVertex> vertices;
long nodeId = node.getId();
if (multiLevelNodes.containsKey(nodeId)) {
vertices = multiLevelNodes.get(nodeId);
} else {
vertices = new HashMap<OSMLevel, IntersectionVertex>();
multiLevelNodes.put(nodeId, vertices);
}
if (!vertices.containsKey(level)) {
Coordinate coordinate = getCoordinate(node);
String label = this.getLevelNodeLabel(node, level);
IntersectionVertex vertex = new IntersectionVertex(graph, label, coordinate.x,
coordinate.y, label);
vertices.put(level, vertex);
// multilevel nodes should also undergo turn-conversion
endpoints.add(vertex);
return vertex;
}
return vertices.get(level);
}
/**
* Make or get a shared vertex for flat intersections, or one vertex per level for multilevel nodes like elevators. When there is an elevator
* or other Z-dimension discontinuity, a single node can appear in several ways at different levels.
*
* @param node The node to fetch a label for.
* @param way The way it is connected to (for fetching level information).
* @return vertex The graph vertex.
*/
private IntersectionVertex getVertexForOsmNode(OSMNode node, OSMWithTags way) {
// If the node should be decomposed to multiple levels,
// use the numeric level because it is unique, the human level may not be (although
// it will likely lead to some head-scratching if it is not).
IntersectionVertex iv = null;
if (node.isMultiLevel()) {
// make a separate node for every level
return recordLevel(node, way);
}
// single-level case
long nid = node.getId();
iv = intersectionNodes.get(nid);
if (iv == null) {
Coordinate coordinate = getCoordinate(node);
String label = getNodeLabel(node);
String highway = node.getTag("highway");
if ("motorway_junction".equals(highway)) {
String ref = node.getTag("ref");
if (ref != null) {
ExitVertex ev = new ExitVertex(graph, label, coordinate.x, coordinate.y);
ev.setExitName(ref);
iv = ev;
}
}
if (iv == null) {
iv = new IntersectionVertex(graph, label, coordinate.x, coordinate.y, label);
if (node.hasTrafficLight()) {
iv.setTrafficLight(true);
}
}
intersectionNodes.put(nid, iv);
endpoints.add(iv);
}
return iv;
}
@Override
public void doneRelations() {
// nothing to do here
}
}
@Override
public void checkInputs() {
for (OpenStreetMapProvider provider : _providers) {
provider.checkInputs();
}
}
}