/*
     * 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"));
          }
        }
      }
    }
    public void addRelation(OSMRelation relation) {
      if (_relations.containsKey(relation.getId())) return;

      if (!(relation.isTag("type", "restriction"))
          && !(relation.isTag("type", "route") && relation.isTag("route", "road"))
          && !(relation.isTag("type", "multipolygon") && relation.hasTag("highway"))
          && !(relation.isTag("type", "level_map"))) {
        return;
      }

      _relations.put(relation.getId(), relation);

      if (_relations.size() % 100 == 0) _log.debug("relations=" + _relations.size());
    }
    /**
     * 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);
              }
            }
          }
        }
      }
    }