All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opentripplanner.graph_builder.module.osm.OsmDatabase Maven / Gradle / Ivy

The newest version!
package org.opentripplanner.graph_builder.module.osm;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import gnu.trove.iterator.TLongIterator;
import gnu.trove.list.TLongList;
import gnu.trove.map.TLongObjectMap;
import gnu.trove.map.hash.TLongObjectHashMap;
import gnu.trove.set.TLongSet;
import gnu.trove.set.hash.TLongHashSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;
import org.opentripplanner.framework.collection.TroveUtils;
import org.opentripplanner.framework.geometry.GeometryUtils;
import org.opentripplanner.framework.geometry.HashGridSpatialIndex;
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
import org.opentripplanner.graph_builder.issue.api.Issue;
import org.opentripplanner.graph_builder.issues.DisconnectedOsmNode;
import org.opentripplanner.graph_builder.issues.InvalidOsmGeometry;
import org.opentripplanner.graph_builder.issues.LevelAmbiguous;
import org.opentripplanner.graph_builder.issues.TurnRestrictionBad;
import org.opentripplanner.graph_builder.issues.TurnRestrictionException;
import org.opentripplanner.graph_builder.issues.TurnRestrictionUnknown;
import org.opentripplanner.graph_builder.module.osm.TurnRestrictionTag.Direction;
import org.opentripplanner.osm.model.OsmEntity;
import org.opentripplanner.osm.model.OsmLevel;
import org.opentripplanner.osm.model.OsmLevel.Source;
import org.opentripplanner.osm.model.OsmNode;
import org.opentripplanner.osm.model.OsmRelation;
import org.opentripplanner.osm.model.OsmRelationMember;
import org.opentripplanner.osm.model.OsmTag;
import org.opentripplanner.osm.model.OsmWay;
import org.opentripplanner.street.model.RepeatingTimePeriod;
import org.opentripplanner.street.model.StreetTraversalPermission;
import org.opentripplanner.street.model.TurnRestrictionType;
import org.opentripplanner.street.search.TraverseMode;
import org.opentripplanner.street.search.TraverseModeSet;
import org.opentripplanner.utils.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OsmDatabase {

  private static final Logger LOG = LoggerFactory.getLogger(OsmDatabase.class);

  private final DataImportIssueStore issueStore;

  /* Map of all nodes used in ways/areas keyed by their OSM ID */
  private final TLongObjectMap nodesById = new TLongObjectHashMap<>();

  /* Map of all bike parking nodes, keyed by their OSM ID */
  private final TLongObjectMap bikeParkingNodes = new TLongObjectHashMap<>();

  /* Map of all bike parking nodes, keyed by their OSM ID */
  private final TLongObjectMap carParkingNodes = new TLongObjectHashMap<>();

  /* Map of all non-area ways keyed by their OSM ID */
  private final TLongObjectMap waysById = new TLongObjectHashMap<>();

  /* Map of all area ways keyed by their OSM ID */
  private final TLongObjectMap areaWaysById = new TLongObjectHashMap<>();

  /* Map of all relations keyed by their OSM ID */
  private final TLongObjectMap relationsById = new TLongObjectHashMap<>();

  /* All walkable areas */
  private final List walkableAreas = new ArrayList<>();

  /* All P+R areas */
  private final List parkAndRideAreas = new ArrayList<>();

  /* All bike parking areas */
  private final List bikeParkingAreas = new ArrayList<>();

  /* Map of all area OSMWay for a given node */
  private final TLongObjectMap> areasForNode = new TLongObjectHashMap<>();

  /* Map of all area OSMWay for a given node */
  private final List singleWayAreas = new ArrayList<>();

  private final Set processedAreas = new HashSet<>();

  /* Set of area way IDs */
  private final TLongSet areaWayIds = new TLongHashSet();

  /* Set of all node IDs of kept ways. Needed to mark which nodes to keep in stage 3. */
  private final TLongSet waysNodeIds = new TLongHashSet();

  /* Set of all node IDs of kept areas. Needed to mark which nodes to keep in stage 3. */
  private final TLongSet areaNodeIds = new TLongHashSet();

  /* Track which vertical level each OSM way belongs to, for building elevators etc. */
  private final Map wayLevels = new HashMap<>();

  /* Set of turn restrictions for each turn "from" way ID */
  private final Multimap turnRestrictionsByFromWay =
    ArrayListMultimap.create();

  /* Set of turn restrictions for each turn "to" way ID */
  private final Multimap turnRestrictionsByToWay =
    ArrayListMultimap.create();

  /*
   * Map of all transit stop nodes that lie within an area and which are connected to the area by
   * a relation. Keyed by the area's OSM way.
   */
  private final Multimap stopsInAreas = HashMultimap.create();

  /*
   * ID of the next virtual node we create during building phase. Negative to prevent conflicts
   * with existing ones.
   */
  private long virtualNodeId = -100000;

  /**
   * 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.
   */
  public boolean noZeroLevels = true;

  public OsmDatabase(DataImportIssueStore issueStore) {
    this.issueStore = issueStore;
  }

  public OsmNode getNode(Long nodeId) {
    return nodesById.get(nodeId);
  }

  public OsmWay getWay(Long nodeId) {
    return waysById.get(nodeId);
  }

  public Collection getWays() {
    return Collections.unmodifiableCollection(waysById.valueCollection());
  }

  public boolean isAreaWay(Long wayId) {
    return areaWayIds.contains(wayId);
  }

  public int nodeCount() {
    return nodesById.size();
  }

  public int wayCount() {
    return waysById.size();
  }

  public Collection getBikeParkingNodes() {
    return Collections.unmodifiableCollection(bikeParkingNodes.valueCollection());
  }

  public Collection getCarParkingNodes() {
    return Collections.unmodifiableCollection(carParkingNodes.valueCollection());
  }

  public Collection getWalkableAreas() {
    return Collections.unmodifiableCollection(walkableAreas);
  }

  public Collection getParkAndRideAreas() {
    return Collections.unmodifiableCollection(parkAndRideAreas);
  }

  public Collection getBikeParkingAreas() {
    return Collections.unmodifiableCollection(bikeParkingAreas);
  }

  public Collection getTurnRestrictionWayIds() {
    return Collections.unmodifiableCollection(turnRestrictionsByFromWay.keySet());
  }

  public Collection getFromWayTurnRestrictions(Long fromWayId) {
    return turnRestrictionsByFromWay.get(fromWayId);
  }

  public Collection getToWayTurnRestrictions(Long toWayId) {
    return turnRestrictionsByToWay.get(toWayId);
  }

  public Collection getStopsInArea(OsmEntity areaParent) {
    return stopsInAreas.get(areaParent);
  }

  public OsmLevel getLevelForWay(OsmEntity way) {
    return Objects.requireNonNullElse(wayLevels.get(way), OsmLevel.DEFAULT);
  }

  public Set getAreasForNode(Long nodeId) {
    Set areas = areasForNode.get(nodeId);
    if (areas == null) {
      return Set.of();
    }
    return areas;
  }

  public boolean isNodeBelongsToWay(Long nodeId) {
    return waysNodeIds.contains(nodeId);
  }

  public void addNode(OsmNode node) {
    if (node.isBikeParking()) {
      bikeParkingNodes.put(node.getId(), node);
    }
    if (node.isParkAndRide()) {
      carParkingNodes.put(node.getId(), node);
    }
    if (
      !(waysNodeIds.contains(node.getId()) ||
        areaNodeIds.contains(node.getId()) ||
        node.isBoardingLocation())
    ) {
      return;
    }

    if (nodesById.containsKey(node.getId())) {
      return;
    }
    nodesById.put(node.getId(), node);
  }

  public void addWay(OsmWay way) {
    /* only add ways once */
    long wayId = way.getId();
    if (waysById.containsKey(wayId) || areaWaysById.containsKey(wayId)) {
      return;
    }

    if (areaWayIds.contains(wayId)) {
      areaWaysById.put(wayId, way);
    }

    /* filter out ways that are not relevant for routing */
    if (
      !(way.isRoutable() || way.isParkAndRide() || way.isBikeParking() || way.isBoardingLocation())
    ) {
      return;
    }

    applyLevelsForWay(way);

    if (way.isRoutableArea()) {
      // 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);
        way
          .getNodeRefs()
          .forEach(node -> {
            TroveUtils.addToMapSet(areasForNode, node, way);
            return true;
          });
      }
      return;
    }

    waysById.put(wayId, way);
  }

  public void addRelation(OsmRelation relation) {
    if (relationsById.containsKey(relation.getId())) {
      return;
    }

    if (
      (relation.isMultiPolygon() && (relation.isRoutable() || relation.isParkAndRide())) ||
      relation.isBikeParking()
    ) {
      // 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 (!relation.isRoutable() && !relation.isParkAndRide() && !relation.isBikeParking()) {
        return;
      }
      for (OsmRelationMember member : relation.getMembers()) {
        areaWayIds.add(member.getRef());
      }
      applyLevelsForWay(relation);
    } else if (
      !relation.isRestriction() &&
      !relation.isRoadRoute() &&
      !(relation.isMultiPolygon() && relation.isRoutable()) &&
      !relation.isLevelMap() &&
      !relation.isStopArea() &&
      !(relation.isRoadRoute() || relation.isBicycleRoute())
    ) {
      return;
    }

    relationsById.put(relation.getId(), relation);
  }

  public void doneFirstPhaseRelations() {
    // nothing to do here
  }

  public void doneSecondPhaseWays() {
    // 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(waysById.valueCollection(), waysNodeIds);
    markNodesForKeeping(areaWaysById.valueCollection(), areaNodeIds);
  }

  public void doneThirdPhaseNodes() {
    processMultipolygonRelations();
    processSingleWayAreas();
  }

  /**
   * After all loading is done (from multiple OSM sources), post-process.
   */
  public void postLoad() {
    // handle turn restrictions, road names, and level maps in relations
    processRelations();

    // intersect non connected areas with ways
    processUnconnectedAreas();
  }

  /**
   * Check if a point is within an epsilon of a node.
   */
  private static boolean checkIntersectionDistance(Point p, OsmNode n, double epsilon) {
    return Math.abs(p.getY() - n.lat) < epsilon && Math.abs(p.getX() - n.lon) < epsilon;
  }

  /**
   * Check if two nodes are within an epsilon.
   */
  private static boolean checkDistanceWithin(OsmNode a, OsmNode b, double epsilon) {
    return Math.abs(a.lat - b.lat) < epsilon && Math.abs(a.lon - b.lon) < epsilon;
  }

  /**
   * Connect areas with ways when unconnected (areas outer rings crossing with ways at the same
   * level, but with no common nodes). Currently process P+R areas only, but could easily be
   * extended to others areas as well.
   */
  private void processUnconnectedAreas() {
    LOG.info("Intersecting unconnected areas...");

    /*
     * Create a spatial index for each segment of area outer rings. Note: The spatial index is
     * temporary and store only areas, so it should not take that much memory. Note 2: For
     * common nodes shared by different ways of different areas we only add them once, otherwise
     * we could end-up looping on creating new intersections.
     */
    Set commonSegments = new HashSet<>();
    HashGridSpatialIndex spndx = new HashGridSpatialIndex<>();
    for (OsmArea area : Iterables.concat(parkAndRideAreas, bikeParkingAreas)) {
      for (Ring ring : area.outermostRings) {
        processAreaRingForUnconnectedAreas(commonSegments, spndx, area, ring);
      }
    }

    // For each way, intersect with areas
    int nCreatedNodes = 0;
    for (OsmWay way : waysById.valueCollection()) {
      OsmLevel wayLevel = getLevelForWay(way);

      // For each segment of the way
      for (int i = 0; i < way.getNodeRefs().size() - 1; i++) {
        OsmNode nA = nodesById.get(way.getNodeRefs().get(i));
        OsmNode nB = nodesById.get(way.getNodeRefs().get(i + 1));
        if (nA == null || nB == null) {
          continue;
        }

        Envelope env = new Envelope(nA.lon, nB.lon, nA.lat, nB.lat);
        List ringSegments = spndx.query(env);
        if (ringSegments.size() == 0) {
          continue;
        }
        LineString seg = GeometryUtils.makeLineString(nA.lon, nA.lat, nB.lon, nB.lat);

        for (RingSegment ringSegment : ringSegments) {
          boolean wayWasSplit = false;

          // Skip if both segments share a common node
          if (
            ringSegment.nA.getId() == nA.getId() ||
            ringSegment.nA.getId() == nB.getId() ||
            ringSegment.nB.getId() == nA.getId() ||
            ringSegment.nB.getId() == nB.getId()
          ) {
            continue;
          }

          // Skip if area and way are from "incompatible" levels
          OsmLevel areaLevel = getLevelForWay(ringSegment.area.parent);
          if (!wayLevel.equals(areaLevel)) {
            continue;
          }

          // Check for real intersection
          LineString seg2 = GeometryUtils.makeLineString(
            ringSegment.nA.lon,
            ringSegment.nA.lat,
            ringSegment.nB.lon,
            ringSegment.nB.lat
          );
          Geometry intersection = seg2.intersection(seg);
          Point p = null;
          if (intersection.isEmpty()) {
            continue;
          } else if (intersection instanceof Point) {
            p = (Point) intersection;
          } else {
            /*
             * This should never happen (intersection between two lines should be a
             * point or a multi-point).
             */
            LOG.error(
              "Alien intersection type between {} ({}--{}) and {} ({}--{}): {}",
              way,
              nA,
              nB,
              ringSegment.area.parent,
              ringSegment.nA,
              ringSegment.nB,
              intersection
            );
            continue;
          }

          // if the intersection is extremely close to one of the nodes of the road or the parking lot, just use that node
          // rather than splitting anything. See issue 1605.
          OsmNode splitNode;
          double epsilon = 0.0000001;

          // note that the if . . . else if structure of this means that if a node at one end of a (way|ring) segment is snapped,
          // the node at the other end cannot be, which is fine because the only time that could happen anyhow
          // would be if the nodes were duplicates.
          // prefer inserting into the ring segment to inserting into the way, so as to reduce graph complexity
          if (checkIntersectionDistance(p, nA, epsilon)) {
            // insert node A into the ring segment
            splitNode = nA;

            // This node is already a part of this ring (perhaps we inserted it
            // previously). No need to connect again. Note that this may not be a safe
            // assumption to make in all cases; suppose a way were to cross exactly
            // over a node *twice*, we would only add it the first time.
            if (ringSegment.ring.nodes.contains(splitNode)) {
              continue;
            }

            if (
              checkDistanceWithin(ringSegment.nA, nA, epsilon) ||
              checkDistanceWithin(ringSegment.nB, nA, epsilon)
            ) {
              issueStore.add(new DisconnectedOsmNode(nA, way, ringSegment.area.parent));
            }
          } else if (checkIntersectionDistance(p, nB, epsilon)) {
            // insert node B into the ring segment
            splitNode = nB;

            if (ringSegment.ring.nodes.contains(splitNode)) {
              continue;
            }

            if (
              checkDistanceWithin(ringSegment.nA, nB, epsilon) ||
              checkDistanceWithin(ringSegment.nB, nB, epsilon)
            ) {
              issueStore.add(new DisconnectedOsmNode(nB, way, ringSegment.area.parent));
            }
          } else if (checkIntersectionDistance(p, ringSegment.nA, epsilon)) {
            // insert node A into the road, if it's not already there

            // don't insert the same node twice. This is not always safe; suppose a way crosses over the same node in the parking area twice.
            // but we assume it doesn't (and even if it does, it's not a huge deal, as it is still connected elsewhere on the same way).
            if (way.getNodeRefs().contains(ringSegment.nA.getId())) continue;

            way.addNodeRef(ringSegment.nA.getId(), i + 1);

            if (
              checkDistanceWithin(ringSegment.nA, nA, epsilon) ||
              checkDistanceWithin(ringSegment.nA, nB, epsilon)
            ) {
              issueStore.add(new DisconnectedOsmNode(nB, ringSegment.area.parent, way));
            }
            // restart loop over way segments as we may have more intersections
            // as we haven't modified the ring, there is no need to modify the spatial index, so breaking here is fine
            i--;
            break;
          } else if (checkIntersectionDistance(p, ringSegment.nB, epsilon)) {
            // insert node B into the road, if it's not already there

            if (way.getNodeRefs().contains(ringSegment.nB.getId())) continue;

            way.addNodeRef(ringSegment.nB.getId(), i + 1);

            if (
              checkDistanceWithin(ringSegment.nB, nA, epsilon) ||
              checkDistanceWithin(ringSegment.nB, nB, epsilon)
            ) {
              issueStore.add(new DisconnectedOsmNode(ringSegment.nB, ringSegment.area.parent, way));
            }
            i--;
            break;
          } else {
            // create a node
            splitNode = createVirtualNode(p.getCoordinate());
            nCreatedNodes++;
            LOG.debug(
              "Adding virtual {}, intersection of {} ({}--{}) and area {} ({}--{}) at {}.",
              splitNode,
              way,
              nA,
              nB,
              ringSegment.area.parent,
              ringSegment.nA,
              ringSegment.nB,
              p
            );
            way.addNodeRef(splitNode.getId(), i + 1);

            /*
             * If we split the way, re-start the way segments loop as the newly created segments
             * could be intersecting again (in case one segment cut many others).
             */
            wayWasSplit = true;
          }

          /*
           * The line below is O(n^2) but we do not insert often and ring size should be
           * rather small.
           */
          int j = ringSegment.ring.nodes.indexOf(ringSegment.nB);
          ringSegment.ring.nodes.add(j, splitNode);

          /*
           * Update spatial index as we just split a ring segment. Note: we do not update
           * the first segment envelope, but as the new envelope is smaller than the
           * previous one this is harmless, apart from increasing a bit false positives
           * count.
           */
          RingSegment ringSegment2 = new RingSegment();
          ringSegment2.area = ringSegment.area;
          ringSegment2.ring = ringSegment.ring;
          ringSegment2.nA = splitNode;
          ringSegment2.nB = ringSegment.nB;
          Envelope env2 = new Envelope(
            ringSegment2.nA.lon,
            ringSegment2.nB.lon,
            ringSegment2.nA.lat,
            ringSegment2.nB.lat
          );
          spndx.insert(env2, ringSegment2);
          ringSegment.nB = splitNode;

          // if we split the way, backtrack over it again to check for additional splits
          // otherwise, we just continue the loop over ring segments
          if (wayWasSplit) {
            i--;
            break;
          }
        }
      }
    }
    LOG.info("Created {} virtual intersection nodes.", nCreatedNodes);
  }

  private void processAreaRingForUnconnectedAreas(
    Set commonSegments,
    HashGridSpatialIndex spndx,
    OsmArea area,
    Ring ring
  ) {
    for (int j = 0; j < ring.nodes.size(); j++) {
      RingSegment ringSegment = new RingSegment();
      ringSegment.area = area;
      ringSegment.ring = ring;
      ringSegment.nA = ring.nodes.get(j);
      ringSegment.nB = ring.nodes.get((j + 1) % ring.nodes.size());
      Envelope env = new Envelope(
        ringSegment.nA.lon,
        ringSegment.nB.lon,
        ringSegment.nA.lat,
        ringSegment.nB.lat
      );
      var key1 = new KeyPair(ringSegment.nA.getId(), ringSegment.nB.getId());
      var key2 = new KeyPair(ringSegment.nB.getId(), ringSegment.nA.getId());
      if (!commonSegments.contains(key1) && !commonSegments.contains(key2)) {
        spndx.insert(env, ringSegment);
        commonSegments.add(key1);
        commonSegments.add(key2);
      }
    }

    ring
      .getHoles()
      .forEach(hole -> processAreaRingForUnconnectedAreas(commonSegments, spndx, area, hole));
  }

  /**
   * Create a virtual OSM node, using a negative unique ID.
   *
   * @param c The location of the node to create.
   * @return The created node.
   */
  private OsmNode createVirtualNode(Coordinate c) {
    OsmNode node = new OsmNode();
    node.lon = c.x;
    node.lat = c.y;
    node.setId(virtualNodeId);
    virtualNodeId--;
    waysNodeIds.add(node.getId());
    nodesById.put(node.getId(), node);
    return node;
  }

  private void applyLevelsForWay(OsmEntity 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,
          issueStore,
          way
        );
      } else if (way.hasTag("layer")) {
        levelName = way.getTag("layer");
        level = OsmLevel.fromString(
          levelName,
          OsmLevel.Source.LAYER_TAG,
          noZeroLevels,
          issueStore,
          way
        );
      }
      if (level == null || (!level.reliable)) {
        issueStore.add(new LevelAmbiguous(levelName, way));
        level = OsmLevel.DEFAULT;
      }
      wayLevels.put(way, level);
    }
  }

  private void markNodesForKeeping(Collection osmWays, TLongSet nodeSet) {
    for (OsmWay way : osmWays) {
      // Since the way is kept, update nodes-with-neighbors
      TLongList nodes = way.getNodeRefs();
      if (nodes.size() > 1) {
        nodeSet.addAll(nodes);
      }
    }
  }

  /**
   * Create areas from single ways.
   */
  private void processSingleWayAreas() {
    AREA: for (OsmWay way : singleWayAreas) {
      if (processedAreas.contains(way)) {
        continue;
      }
      TLongIterator longIterator = way.getNodeRefs().iterator();
      while (longIterator.hasNext()) {
        long nodeRef = longIterator.next();
        if (!nodesById.containsKey(nodeRef)) {
          continue AREA;
        }
      }
      try {
        addArea(new OsmArea(way, List.of(way), Collections.emptyList(), nodesById));
      } catch (OsmArea.AreaConstructionException | Ring.RingConstructionException 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.
        issueStore.add(new InvalidOsmGeometry(way));
      } catch (IllegalArgumentException iae) {
        // This occurs when there are an invalid number of points in a LinearRing
        // Mark the ring as processed so we don't retry it.
        issueStore.add(new InvalidOsmGeometry(way));
      }
      processedAreas.add(way);
    }
  }

  /**
   * 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 : relationsById.valueCollection()) {
      if (processedAreas.contains(relation)) {
        continue;
      }
      if (
        !(relation.isMultiPolygon() &&
          (relation.isRoutable() || relation.isParkAndRide() || relation.isBikeParking()))
      ) {
        continue;
      }
      // Area multipolygons -- pedestrian plazas
      ArrayList innerWays = new ArrayList<>();
      ArrayList outerWays = new ArrayList<>();
      for (OsmRelationMember member : relation.getMembers()) {
        OsmWay way = areaWaysById.get(member.getRef());
        if (way == null) {
          // relation includes way which does not exist in the data. Skip.
          continue RELATION;
        }
        TLongIterator wayNodeIterator = way.getNodeRefs().iterator();
        while (wayNodeIterator.hasNext()) {
          long nodeId = wayNodeIterator.next();
          if (nodesById.containsKey(nodeId)) {
            TroveUtils.addToMapSet(areasForNode, nodeId, way);
          } else {
            // 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;
          }
        }
        if (member.hasRoleInner()) {
          innerWays.add(way);
        } else if (member.hasRoleOuter()) {
          outerWays.add(way);
        } else {
          LOG.warn("Unexpected role '{}' in multipolygon", member.getRole());
        }
      }
      processedAreas.add(relation);
      try {
        addArea(new OsmArea(relation, outerWays, innerWays, nodesById));
      } catch (OsmArea.AreaConstructionException | Ring.RingConstructionException e) {
        issueStore.add(new InvalidOsmGeometry(relation));
        continue;
      }

      for (OsmRelationMember member : relation.getMembers()) {
        // multipolygons for attribute mapping
        if (!(member.hasTypeWay() && waysById.containsKey(member.getRef()))) {
          continue;
        }

        OsmEntity way = waysById.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.isRailwayPlatform() && !way.hasTag("railway")) {
          way.addTag("railway", "platform");
        }
        if (relation.isPlatform() && !way.hasTag("public_transport")) {
          way.addTag("public_transport", "platform");
        }
      }
    }
  }

  /**
   * Handler for a new OsmArea (single way area or multipolygon relations)
   */
  private void addArea(OsmArea area) {
    StreetTraversalPermission permissions = area.parent
      .getOsmProvider()
      .getWayPropertySet()
      .getDataForWay(area.parent)
      .getPermission();
    if (area.parent.isRoutable() && permissions != StreetTraversalPermission.NONE) {
      walkableAreas.add(area);
    }
    // Please note: the same area can be both car P+R AND bike park.
    if (area.parent.isParkAndRide()) {
      parkAndRideAreas.add(area);
    }
    if (area.parent.isBikeParking()) {
      bikeParkingAreas.add(area);
    }
  }

  /**
   * Copies useful metadata from relations to the relevant ways/nodes.
   */
  private void processRelations() {
    LOG.debug("Processing relations...");

    for (OsmRelation relation : relationsById.valueCollection()) {
      if (relation.isRestriction()) {
        processRestriction(relation);
      } else if (relation.isLevelMap()) {
        processLevelMap(relation);
      } else if (relation.isRoute()) {
        processRoute(relation);
      } else if (relation.isPublicTransport()) {
        processPublicTransportStopArea(relation);
      }
    }
  }

  /**
   * Handle route=bicycle relations. Copies their network type to all way members.
   *
   * @see "https://wiki.openstreetmap.org/wiki/Tag:route%3Dbicycle"
   */
  private void processBicycleRoute(OsmRelation relation) {
    if (relation.isBicycleRoute()) {
      // we treat networks without known network type like local networks
      var network = relation.getTagOpt("network").orElse("lcn");
      setNetworkForAllMembers(relation, network);
    }
  }

  private void setNetworkForAllMembers(OsmRelation relation, String key) {
    relation
      .getMembers()
      .forEach(member -> {
        var isOsmWay = member.hasTypeWay();
        var way = waysById.get(member.getRef());
        // if it is an OSM way (rather than a node) and it doesn't already contain the tag
        // we add it
        if (way != null && isOsmWay && !way.hasTag(key)) {
          way.addTag(key, "yes");
        }
      });
  }

  /**
   * Store turn restrictions.
   */
  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) {
      issueStore.add(
        new TurnRestrictionBad(relation.getId(), "One of from|via|to edges are empty in relation")
      );
      return;
    }

    TraverseModeSet modes = new TraverseModeSet(TraverseMode.BICYCLE, TraverseMode.CAR);
    String exceptModes = relation.getTag("except");
    if (exceptModes != null) {
      for (String m : exceptModes.split(";")) {
        if (m.equals("motorcar")) {
          modes.setCar(false);
        } else if (m.equals("bicycle")) {
          modes.setBicycle(false);
          issueStore.add(new TurnRestrictionException(via, from));
        }
      }
    }

    TurnRestrictionTag tag;
    if (relation.isTag("restriction", "no_right_turn")) {
      tag = new TurnRestrictionTag(
        via,
        TurnRestrictionType.NO_TURN,
        Direction.RIGHT,
        relation.getId()
      );
    } else if (relation.isTag("restriction", "no_left_turn")) {
      tag = new TurnRestrictionTag(
        via,
        TurnRestrictionType.NO_TURN,
        Direction.LEFT,
        relation.getId()
      );
    } else if (relation.isTag("restriction", "no_straight_on")) {
      tag = new TurnRestrictionTag(
        via,
        TurnRestrictionType.NO_TURN,
        Direction.STRAIGHT,
        relation.getId()
      );
    } else if (relation.isTag("restriction", "no_u_turn")) {
      tag = new TurnRestrictionTag(via, TurnRestrictionType.NO_TURN, Direction.U, relation.getId());
    } else if (relation.isTag("restriction", "only_straight_on")) {
      tag = new TurnRestrictionTag(
        via,
        TurnRestrictionType.ONLY_TURN,
        Direction.STRAIGHT,
        relation.getId()
      );
    } else if (relation.isTag("restriction", "only_right_turn")) {
      tag = new TurnRestrictionTag(
        via,
        TurnRestrictionType.ONLY_TURN,
        Direction.RIGHT,
        relation.getId()
      );
    } else if (relation.isTag("restriction", "only_left_turn")) {
      tag = new TurnRestrictionTag(
        via,
        TurnRestrictionType.ONLY_TURN,
        Direction.LEFT,
        relation.getId()
      );
    } else if (relation.isTag("restriction", "only_u_turn")) {
      tag = new TurnRestrictionTag(
        via,
        TurnRestrictionType.ONLY_TURN,
        Direction.U,
        relation.getId()
      );
    } else {
      issueStore.add(new TurnRestrictionUnknown(relation, 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"),
          relation.getOsmProvider()::getZoneId
        );
      } catch (NumberFormatException e) {
        LOG.info("Unparseable turn restriction: {}", relation.getId());
      }
    }

    turnRestrictionsByFromWay.put(from, tag);
    turnRestrictionsByToWay.put(to, tag);
  }

  /**
   * Process an OSM level map.
   */
  private void processLevelMap(OsmRelation relation) {
    var levelsTag = relation.getTag("levels");
    if (!StringUtils.hasValue(levelsTag)) {
      issueStore.add(
        Issue.issue(
          "InvalidLevelMap",
          "Could not parse level map for osm relation %d as it was malformed. Skipped.",
          relation.getId()
        )
      );
      return;
    }

    Map levels = OsmLevel.mapFromSpecList(
      levelsTag,
      Source.LEVEL_MAP,
      true,
      issueStore,
      relation
    );
    for (OsmRelationMember member : relation.getMembers()) {
      if (member.hasTypeWay() && waysById.containsKey(member.getRef())) {
        OsmWay way = waysById.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("{} has undefined level {}", member.getRef(), role);
            }
          }
        }
      }
    }
  }

  /**
   * Handle route=road and route=bicycle relations.
   */
  private void processRoute(OsmRelation relation) {
    for (OsmRelationMember member : relation.getMembers()) {
      if (!(member.hasTypeWay() && waysById.containsKey(member.getRef()))) {
        continue;
      }

      OsmEntity way = waysById.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")));
        }
      }
    }
    processBicycleRoute(relation);
  }

  /**
   * Process an OSM public transport stop area relation.
   * 

* This goes through all public_transport=stop_area relations and adds the parent (either an area * or multipolygon relation) as the key and a Set of transit stop nodes that should be included in * the parent area as the value into stopsInAreas. This improves {@link org.opentripplanner.graph_builder.module.OsmBoardingLocationsModule} * by enabling us to have unconnected stop nodes within the areas by creating relations. * * @author hannesj * @see "http://wiki.openstreetmap.org/wiki/Tag:public_transport%3Dstop_area" */ private void processPublicTransportStopArea(OsmRelation relation) { Set platformAreas = new HashSet<>(); Set platformNodes = new HashSet<>(); for (OsmRelationMember member : relation.getMembers()) { switch (member.getType()) { case NODE -> { var node = nodesById.get(member.getRef()); if (node != null && (node.isEntrance() || node.isBoardingLocation())) { platformNodes.add(node); } } case WAY -> { if (member.hasRolePlatform() && areaWaysById.containsKey(member.getRef())) { platformAreas.add(areaWaysById.get(member.getRef())); } } case RELATION -> { if (member.hasRolePlatform() && relationsById.containsKey(member.getRef())) { platformAreas.add(relationsById.get(member.getRef())); } } } } for (OsmEntity area : platformAreas) { if (area == null) { throw new RuntimeException( "Could not process public transport relation '%s' (%s)".formatted( relation, relation.url() ) ); } // single platform area presumably contains only one level in most cases // a node inside it may specify several levels if it is an elevator // make sure each node has access to the current platform level final Set filterLevels = area.getLevels(); platformNodes .stream() .filter(node -> node.getLevels().containsAll(filterLevels)) .forEach(node -> stopsInAreas.put(area, node)); } } private String addUniqueName(String routes, String name) { String[] names = routes.split(", "); for (String existing : names) { if (existing.equals(name)) { return routes; } } return routes + ", " + name; } // Simple holder for the spatial index static class RingSegment { OsmArea area; Ring ring; OsmNode nA; OsmNode nB; } private record KeyPair(long id0, long id1) {} }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy