/*
     * 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;
        }

        OSMWay 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")));
          }
        }
      }
    }
    /**
     * <<<<<<< HEAD ======= Copies useful metadata from multipolygon relations to the relevant ways.
     *
     * <p>This is done at a different time than processRelations(), so that way purging doesn't
     * remove the used ways.
     */
    private void processMultipolygons() {
      for (OSMRelation relation : _relations.values()) {
        if (!(relation.isTag("type", "multipolygon") && relation.hasTag("highway"))) {
          continue;
        }

        for (OSMRelationMember member : relation.getMembers()) {
          if (!("way".equals(member.getType()) && _ways.containsKey(member.getRef()))) {
            continue;
          }

          OSMWay way = _ways.get(member.getRef());
          if (way == null) {
            continue;
          }

          if (relation.hasTag("highway") && !way.hasTag("highway")) {
            way.addTag("highway", relation.getTag("highway"));
          }
          if (relation.hasTag("name") && !way.hasTag("name")) {
            way.addTag("name", relation.getTag("name"));
          }
          if (relation.hasTag("ref") && !way.hasTag("ref")) {
            way.addTag("ref", relation.getTag("ref"));
          }
        }
      }
    }
    /**
     * Process an OSM level map.
     *
     * @param relation
     */
    private void processLevelMap(OSMRelation relation) {
      ArrayList<String> levels = new ArrayList<String>();

      // This stores the mapping from level keys to the full level values
      HashMap<String, String> levelFullNames = new HashMap<String, String>();

      int levelDelta = 0;

      /*
       * parse all of the levels
       * this array is ordered
       * this is a little different than the OpenStreetMap levels notation,
       * because the lowest level of a building (basement or whatever) is
       * always otp:numeric_level 0. This is OK, as otp:numeric_level tags
       * are *always* accompanied by otp:human_level tags that give the actual
       * name of the level; otp:numeric_level is used only for relative
       * position, and should *never* be shown to the user. To make things more understandable
       * we try to find a delta to compensate and put the basement where it should be, but
       * this data should never be displayed to the user or mixed with OSM levels
       * from elsewhere. It's possible we wouldn't find a delta (imagine
       * levels=Garage;Basement;Lobby;Sky Bridge;Roof), but this is OK.
       */

      Pattern isRange = Pattern.compile("^[0-9]+\\-[0-9]+$");
      Matcher m;

      for (String level : relation.getTag("levels").split(";")) {
        // split out ranges
        m = isRange.matcher(level);
        if (m.matches()) {
          String[] range = level.split("-");
          int endOfRange = Integer.parseInt(range[1]);
          for (int i = Integer.parseInt(range[0]); i <= endOfRange; i++) {
            levels.add(Integer.toString(i));
          }
        }
        // not a range, just normal
        else {
          levels.add(level);
        }
      }

      // try to get a best-guess delta between level order and level numbers, and fix up
      // levels
      for (int i = 0; i < levels.size(); i++) {
        String level = levels.get(i);
        // leaving it null gives NullPointerException when there is no matched 0 level
        // making it 1 doesn't matter, since its only purpose is to be compared to 0
        Integer numLevel = 1;

        // try to parse out the level number
        try {
          numLevel = Integer.parseInt(level);
        } catch (NumberFormatException e) {
          try {
            numLevel = Integer.parseInt(level.split("=")[0]);
          } catch (NumberFormatException e2) {
            try {
              // http://stackoverflow.com/questions/1181969/java-get-last-element-after-split
              int lastInd = level.lastIndexOf('@');
              if (lastInd != -1) {
                numLevel = Integer.parseInt(level.substring(lastInd + 1));
              }
            } catch (NumberFormatException e3) {
              // do nothing
            }
          }
        }

        if (numLevel == 0) {
          levelDelta = -1 * levels.indexOf(level);
        }

        String levelIndex;
        String levelName;
        // get just the human-readable level name from a name like T=Tunnel@-15
        // first, discard elevation info
        // don't use split, in case there is an @ in the level name; split on only the last
        // one
        int lastIndAt = level.lastIndexOf('@');
        if (lastIndAt >= 1) {
          level = level.substring(0, lastIndAt);
        }

        // if it's there, discard the long name, but put it into a hashmap for retrieval
        // below
        // Why not just use the hashmap? Because we need the ordered ArrayList.
        Integer levelSplit = level.indexOf('=');
        if (levelSplit >= 1) {
          levelIndex = level.substring(0, levelSplit);
          levelName = level.substring(levelSplit + 1);
        } else {
          // set them both the same, the @whatever has already been discarded
          levelIndex = levelName = level;
        }

        // overwrite for later indexing
        levels.set(i, levelIndex);

        // add to the HashMap
        levelFullNames.put(levelIndex, levelName);
      }

      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();

            // this would indicate something more complicated than a single
            // level. Skip it.
            if (!relation.hasTag("role:" + role)) {

              if (levels.indexOf(role) != -1) {
                way.addTag(
                    "otp:numeric_level", Integer.toString(levels.indexOf(role) + levelDelta));
                way.addTag("otp:human_level", levelFullNames.get(role));
              } else {
                _log.warn(member.getRef() + " has undefined level " + role);
              }
            }
          }
        }
      }
    }
    public void buildGraph(Graph graph) {
      this.graph = graph;

      // handle turn restrictions and road names in relations
      processRelations();

      // Remove all simple islands
      _nodes.keySet().retainAll(_nodesWithNeighbors);

      long wayIndex = 0;

      // store levels that cannot be parsed, and assign them a number
      int nextUnparsedLevel = 0;
      HashMap<String, Integer> unparsedLevels = new HashMap<String, Integer>();

      // figure out which nodes that are actually intersections
      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);
          }
        }
      }
      GeometryFactory geometryFactory = new GeometryFactory();

      /* build an ordinary graph, which we will convert to an edge-based graph */

      for (OSMWay way : _ways.values()) {

        if (wayIndex % 10000 == 0) _log.debug("ways=" + wayIndex + "/" + _ways.size());
        wayIndex++;

        WayProperties wayData = wayPropertySet.getDataForWay(way);

        if (!way.hasTag("name")) {
          String creativeName = wayPropertySet.getCreativeNameForWay(way);
          if (creativeName != null) {
            way.addTag("otp:gen_name", creativeName);
          }
        }
        Set<Alert> note = wayPropertySet.getNoteForWay(way);

        StreetTraversalPermission permissions =
            getPermissionsForEntity(way, wayData.getPermission());
        if (permissions == StreetTraversalPermission.NONE) continue;

        List<Long> nodes = way.getNodeRefs();

        IntersectionVertex startEndpoint = null, endEndpoint = null;

        ArrayList<Coordinate> segmentCoordinates = new ArrayList<Coordinate>();

        /* The otp:numeric_level tag adds a constant to level numbers depending on
          where they come from (level map, tags).
          This prevents a layer 1 from being equal to a level_map 1, because they may
          not represent the same thing. Worst case scenario, OTP will say
          "take elevator to level 1" when you're already on level 1.
        */
        final int LEVEL_MAP_LEVEL = 0;
        final int LEVEL_TAG_LEVEL = 100000;
        final int LAYER_TAG_LEVEL = 200000;
        final int OTHER_LEVEL = 300000;
        final int UNPARSED_LEVEL = 400000;

        if (!way.hasTag("otp:numeric_level")) {
          // Parse levels, if a level map was not already applied in processRelations
          String strLevel = null;
          int offset = LEVEL_MAP_LEVEL, intLevel;
          if (way.hasTag("level")) { // TODO: floating-point levels &c.
            strLevel = way.getTag("level");
            offset = LEVEL_TAG_LEVEL;
          } else if (way.hasTag("layer")) {
            strLevel = way.getTag("layer");
            offset = LAYER_TAG_LEVEL;
          }
          if (strLevel != null) {
            try {
              intLevel = Integer.parseInt(strLevel);
            } catch (NumberFormatException e) {
              // could not parse the level string as an integer
              // get a unique level number for this
              if (unparsedLevels.containsKey(strLevel)) {
                intLevel = unparsedLevels.get(strLevel);
              } else { // make a new unique ID
                intLevel = nextUnparsedLevel++;
                unparsedLevels.put(strLevel, intLevel);
              }
              offset = UNPARSED_LEVEL;
              _log.warn(
                  "Could not determine ordinality of layer "
                      + strLevel
                      + ". Elevators will work, but costing may be incorrect. "
                      + "A level map should be used in this situation.");
            }
          } else {
            // no string level description was available. assume ground level, but
            // don't assume it's connected to any other ground level.
            intLevel = 0;
            offset = OTHER_LEVEL;
          }
          if (noZeroLevels && intLevel >= 0) {
            // add 1 to human-readable representation of non-negative levels
            // in (-inf, -1] U [1, inf) locations like the US
            strLevel = Integer.toString(intLevel + 1);
          }
          way.addTag("otp:numeric_level", Integer.toString(intLevel + offset));
          way.addTag("otp:human_level", strLevel); // redunant
          levelHumanNames.put(intLevel + offset, strLevel);
        }

        /*
         * 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. For
         * nodes which are shared, create endpoints and StreetVertex instances.
         */
        Long startNode = null;
        OSMNode osmStartNode = null;
        for (int i = 0; i < nodes.size() - 1; i++) {
          Long endNode = nodes.get(i + 1);
          if (osmStartNode == null) {
            startNode = nodes.get(i);
            osmStartNode = _nodes.get(startNode);
          }
          OSMNode osmEndNode = _nodes.get(endNode);

          if (osmStartNode == null || osmEndNode == null) continue;

          LineString geometry;

          /*
           * skip vertices that are not intersections, except that we use them for
           * geometry
           */
          if (segmentCoordinates.size() == 0) {
            segmentCoordinates.add(getCoordinate(osmStartNode));
          }

          if (intersectionNodes.containsKey(endNode) || i == nodes.size() - 2) {
            segmentCoordinates.add(getCoordinate(osmEndNode));
            geometry =
                geometryFactory.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);
          } else { // subsequent iterations
            startEndpoint = endEndpoint;
          }

          endEndpoint = getVertexForOsmNode(osmEndNode, way);

          P2<PlainStreetEdge> streets =
              getEdgesForStreet(startEndpoint, endEndpoint, way, i, permissions, geometry);

          PlainStreetEdge street = streets.getFirst();

          if (street != null) {
            double safety = wayData.getSafetyFeatures().getFirst();
            street.setBicycleSafetyEffectiveLength(street.getLength() * safety);
            if (safety < bestBikeSafety) {
              bestBikeSafety = safety;
            }
            if (note != null) {
              street.setNote(note);
            }
          }

          PlainStreetEdge backStreet = streets.getSecond();
          if (backStreet != null) {
            double safety = wayData.getSafetyFeatures().getSecond();
            if (safety < bestBikeSafety) {
              bestBikeSafety = safety;
            }
            backStreet.setBicycleSafetyEffectiveLength(backStreet.getLength() * safety);
            if (note != null) {
              backStreet.setNote(note);
            }
          }

          /* 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) {
                TurnRestriction restriction = turnRestrictionsByTag.get(tag);
                restriction.from = backStreet;
              } else if (tag.via == endNode) {
                TurnRestriction restriction = turnRestrictionsByTag.get(tag);
                restriction.from = street;
              }
            }
          }

          restrictionTags = turnRestrictionsByToWay.get(way.getId());
          if (restrictionTags != null) {
            for (TurnRestrictionTag tag : restrictionTags) {
              if (tag.via == startNode) {
                TurnRestriction restriction = turnRestrictionsByTag.get(tag);
                restriction.to = street;
              } else if (tag.via == endNode) {
                TurnRestriction restriction = turnRestrictionsByTag.get(tag);
                restriction.to = backStreet;
              }
            }
          }
          startNode = endNode;
          osmStartNode = _nodes.get(startNode);
        }
      } // END loop over OSM ways

      /* 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<Integer, IntersectionVertex> levels = 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. */

        ArrayList<Vertex> onboardVertices = new ArrayList<Vertex>();

        Integer[] levelKeys = levels.keySet().toArray(new Integer[0]);
        Arrays.sort(levelKeys);

        for (Integer level : levelKeys) {
          // get the source node to hang all this stuff off of.
          String humanLevel = levelHumanNames.get(level);
          IntersectionVertex sourceVert = levels.get(level);
          String sourceVertLabel = sourceVert.getLabel();

          // create a Vertex to connect the FreeNode to.
          ElevatorOffboardVertex offboardVert =
              new ElevatorOffboardVertex(
                  graph, sourceVertLabel + "_middle", sourceVert.getX(), sourceVert.getY());

          new FreeEdge(sourceVert, offboardVert);
          new FreeEdge(offboardVert, sourceVert);

          // Create a vertex to connect the ElevatorAlight, ElevatorBoard, and
          // ElevatorHop edges to.
          ElevatorOnboardVertex onboardVert =
              new ElevatorOnboardVertex(
                  graph, sourceVertLabel + "_onboard", sourceVert.getX(), sourceVert.getY());

          new ElevatorBoardEdge(offboardVert, onboardVert);
          new ElevatorAlightEdge(onboardVert, offboardVert, humanLevel);

          // add it to the array so it can be connected later
          onboardVertices.add(onboardVert);
        }

        // -1 because we loop over it two at a time
        Integer vSize = onboardVertices.size() - 1;

        for (Integer i = 0; i < vSize; i++) {
          Vertex from = onboardVertices.get(i);
          Vertex to = onboardVertices.get(i + 1);

          StreetTraversalPermission permission = StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE;

          // default to true
          boolean wheelchairAccessible = true;
          // 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 theEdge = new ElevatorHopEdge(from, to, permission);
          ElevatorHopEdge backEdge = new ElevatorHopEdge(to, from, permission);
          theEdge.wheelchairAccessible = wheelchairAccessible;
          backEdge.wheelchairAccessible = wheelchairAccessible;
        }
      } // END elevator edge loop

      /* unify turn restrictions */
      Map<Edge, TurnRestriction> turnRestrictions = new HashMap<Edge, TurnRestriction>();
      for (TurnRestriction restriction : turnRestrictionsByTag.values()) {
        turnRestrictions.put(restriction.from, restriction);
      }
      if (customNamer != null) {
        customNamer.postprocess(graph);
      }
      applyBikeSafetyFactor(graph);
      StreetUtils.pruneFloatingIslands(graph);
      StreetUtils.makeEdgeBased(graph, endpoints, turnRestrictions);
    } // END buildGraph()