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

org.opentripplanner.osm.model.OsmEntity Maven / Gradle / Ivy

The newest version!
package org.opentripplanner.osm.model;

import java.time.Duration;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.opentripplanner.framework.i18n.I18NString;
import org.opentripplanner.framework.i18n.NonLocalizedString;
import org.opentripplanner.framework.i18n.TranslatedString;
import org.opentripplanner.graph_builder.module.osm.OsmModule;
import org.opentripplanner.osm.OsmProvider;
import org.opentripplanner.street.model.StreetTraversalPermission;
import org.opentripplanner.transit.model.basic.Accessibility;
import org.opentripplanner.utils.tostring.ToStringBuilder;

/**
 * A base class for OSM entities containing common methods.
 */
public class OsmEntity {

  /**
   * highway=* values that we don't want to even consider when building the graph.
   */
  private static final Set NON_ROUTABLE_HIGHWAYS = Set.of(
    "proposed",
    "planned",
    "construction",
    "razed",
    "raceway",
    "abandoned",
    "historic",
    "no",
    "emergency_bay",
    "rest_area",
    "services",
    "bus_guideway",
    "escape"
  );

  private static final Set INDOOR_ROUTABLE_VALUES = Set.of("corridor", "area");

  private static final Set LEVEL_TAGS = Set.of("level", "layer");
  private static final Set DEFAULT_LEVEL = Set.of("0");
  private static final Consumer NO_OP = i -> {};

  /* To save memory this is only created when an entity actually has tags. */
  private Map tags;

  protected long id;

  protected I18NString creativeName;

  private OsmProvider osmProvider;

  public static boolean isFalse(String tagValue) {
    return ("no".equals(tagValue) || "0".equals(tagValue) || "false".equals(tagValue));
  }

  public static boolean isTrue(String tagValue) {
    return ("yes".equals(tagValue) || "1".equals(tagValue) || "true".equals(tagValue));
  }

  /**
   * Gets the id.
   */
  public long getId() {
    return id;
  }

  /**
   * Sets the id.
   */
  public void setId(long id) {
    this.id = id;
  }

  /**
   * Adds a tag.
   */
  public void addTag(OsmTag tag) {
    if (tags == null) tags = new HashMap<>();

    tags.put(tag.getK().toLowerCase(), tag.getV());
  }

  /**
   * Adds a tag.
   */
  public OsmEntity addTag(String key, String value) {
    if (key == null || value == null) {
      return this;
    }

    if (tags == null) {
      tags = new HashMap<>();
    }

    tags.put(key.toLowerCase(), value);
    return this;
  }

  /**
   * The tags of an entity.
   */
  public Map getTags() {
    return Objects.requireNonNullElse(tags, Map.of());
  }

  /**
   * Is the tag defined?
   */
  public boolean hasTag(String tag) {
    tag = tag.toLowerCase();
    return tags != null && tags.containsKey(tag);
  }

  /**
   * Determines if a tag contains a false value. 'no', 'false', and '0' are considered false.
   */
  public boolean isTagFalse(String tag) {
    tag = tag.toLowerCase();
    if (tags == null) {
      return false;
    }

    return isFalse(getTag(tag));
  }

  /**
   * Returns the level of wheelchair access of the element.
   */
  public Accessibility wheelchairAccessibility() {
    if (isTagTrue("wheelchair")) {
      return Accessibility.POSSIBLE;
    } else if (isTagFalse("wheelchair")) {
      return Accessibility.NOT_POSSIBLE;
    } else {
      return Accessibility.NO_INFORMATION;
    }
  }

  /**
   * Determines if a tag contains a true value. 'yes', 'true', and '1' are considered true.
   */
  public boolean isTagTrue(String tag) {
    tag = tag.toLowerCase();
    if (tags == null) {
      return false;
    }

    return isTrue(getTag(tag));
  }

  /**
   * Returns true if bicycle dismounts are forced.
   */
  public boolean isBicycleDismountForced() {
    return isTag("bicycle", "dismount");
  }

  public boolean isSidewalk() {
    return isTag("footway", "sidewalk") && isTag("highway", "footway");
  }

  protected boolean doesTagAllowAccess(String tag) {
    if (tags == null) {
      return false;
    }
    if (isTagTrue(tag)) {
      return true;
    }
    tag = tag.toLowerCase();
    String value = getTag(tag);
    return (
      "designated".equals(value) ||
      "official".equals(value) ||
      "permissive".equals(value) ||
      "unknown".equals(value)
    );
  }

  /** @return a tag's value, converted to lower case. */
  @Nullable
  public String getTag(String tag) {
    tag = tag.toLowerCase();
    if (tags != null && tags.containsKey(tag)) {
      return tags.get(tag);
    }
    return null;
  }

  /**
   *
   * @return A tags value converted to lower case. An empty Optional if tags is not present.
   */
  public Optional getTagOpt(String network) {
    return Optional.ofNullable(getTag(network));
  }

  /**
   * Get tag and convert it to an integer. If the tag exist, but can not be parsed into a number,
   * then the error handler is called with the value which failed to parse.
   */
  public OptionalInt getTagAsInt(String tag, Consumer errorHandler) {
    String value = getTag(tag);
    if (value != null) {
      try {
        return OptionalInt.of(Integer.parseInt(value));
      } catch (NumberFormatException e) {
        errorHandler.accept(value);
      }
    }
    return OptionalInt.empty();
  }

  /**
   * Parse an OSM duration tag, which is one of:
   *   mm
   *   hh:mm
   *   hh:mm:ss
   * and where the leading value is not limited to any maximum.
   * See OSM wiki definition
   * of duration.
   *
   * @param duration string in format mm, hh:mm, or hh:mm:ss
   * @return Duration
   * @throws DateTimeParseException on bad input
   */
  public static Duration parseOsmDuration(String duration) {
    // Unfortunately DateFormatParserBuilder doesn't quite do enough for this case.
    // It has the capability for expressing optional parts, so it could express hh(:mm(:ss)?)?
    // but it cannot express (hh:)?mm(:ss)? where the existence of (:ss) implies the existence
    // of (hh:). Even if it did, it would not be able to handle the cases where hours are
    // greater than 23 or (if there is no hours part at all) minutes are greater than 59, which
    // are both allowed by the spec and exist in OSM data. Durations are not LocalTimes after
    // all, in parsing a LocalTime it makes sense and is correct that hours cannot be more than
    // 23 or minutes more than 59, but in durations if you have capped the largest unit, it is
    // reasonable for the amount of the largest unit to be as large as it needs to be.
    int colonCount = (int) duration.chars().filter(ch -> ch == ':').count();
    if (colonCount <= 2) {
      try {
        int i, j;
        long hours, minutes, seconds;
        // The first :-separated element can be any width, and has no maximum. It still has
        // to be non-negative. The following elements must be 2 characters wide, non-negative,
        // and less than 60.
        switch (colonCount) {
          // case "m"
          case 0:
            minutes = Long.parseLong(duration);
            if (minutes >= 0) {
              return Duration.ofMinutes(minutes);
            }
            break;
          // case "h:mm"
          case 1:
            i = duration.indexOf(':');
            hours = Long.parseLong(duration.substring(0, i));
            minutes = Long.parseLong(duration.substring(i + 1));
            if (duration.length() - i == 3 && hours >= 0 && minutes >= 0 && minutes < 60) {
              return Duration.ofHours(hours).plusMinutes(minutes);
            }
            break;
          // case "h:mm:ss"
          default:
            i = duration.indexOf(':');
            j = duration.indexOf(':', i + 1);
            hours = Long.parseLong(duration.substring(0, i));
            minutes = Long.parseLong(duration.substring(i + 1, j));
            seconds = Long.parseLong(duration.substring(j + 1));
            if (
              j - i == 3 &&
              duration.length() - j == 3 &&
              hours >= 0 &&
              minutes >= 0 &&
              minutes < 60 &&
              seconds >= 0 &&
              seconds < 60
            ) {
              return Duration.ofHours(hours).plusMinutes(minutes).plusSeconds(seconds);
            }
            break;
        }
      } catch (NumberFormatException e) {
        // fallthrough
      }
    }
    throw new DateTimeParseException("Bad OSM duration", duration, 0);
  }

  /**
   * Gets a tag's value, assumes it is an OSM wiki specified duration, parses and returns it.
   * If parsing fails, calls the error handler.
   *
   * @param key
   * @param errorHandler
   * @return parsed Duration, or empty
   */
  public Optional getTagValueAsDuration(String key, Consumer errorHandler) {
    String value = getTag(key);
    if (value != null) {
      try {
        return Optional.of(parseOsmDuration(value));
      } catch (DateTimeParseException e) {
        errorHandler.accept(value);
      }
    }
    return Optional.empty();
  }

  /**
   * Some tags are allowed to have values like 55, "true" or "false".
   * 

* "true", "yes" is returned as 1. *

* "false", "no" is returned as 0 *

* Everything else is returned as an emtpy optional. */ public OptionalInt parseIntOrBoolean(String tag, Consumer errorHandler) { var maybeInt = getTagAsInt(tag, NO_OP); if (maybeInt.isPresent()) { return maybeInt; } else { if (isTagTrue(tag)) { return OptionalInt.of(1); } else if (isTagFalse(tag)) { return OptionalInt.of(0); } else if (hasTag(tag)) { errorHandler.accept(getTag(tag)); return OptionalInt.empty(); } else { return OptionalInt.empty(); } } } /** * Checks is a tag contains the specified value. */ public boolean isTag(String tag, String value) { tag = tag.toLowerCase(); if (tags != null && tags.containsKey(tag) && value != null) { return value.equals(tags.get(tag)); } return false; } /** * Takes a tag key and checks if the value is any of those in {@code oneOfTags}. */ public boolean isOneOfTags(String key, Set oneOfTags) { return oneOfTags.stream().anyMatch(value -> isTag(key, value)); } /** * Returns a name-like value for an entity (if one exists). The otp: namespaced tags are created * by {@link OsmModule} */ @Nullable public I18NString getAssumedName() { if (tags == null) { return null; } if (tags.containsKey("name")) { return TranslatedString.getI18NString(this.generateI18NForPattern("{name}"), true, false); } if (tags.containsKey("otp:route_name")) { return new NonLocalizedString(tags.get("otp:route_name")); } if (this.creativeName != null) { return this.creativeName; } if (tags.containsKey("otp:route_ref")) { return new NonLocalizedString(tags.get("otp:route_ref")); } if (tags.containsKey("ref")) { return new NonLocalizedString(tags.get("ref")); } return null; } /** * Replace various pattern by the OSM tag values, with I18n support. * * @param pattern Pattern containing options tags to replace, such as "text" or "note: {note}". * Tag names between {} are replaced by the OSM tag value, if it is present (or the * empty string if not). * @return A map language code → text, with at least one entry for the default language, and any * other language found in OSM tag. */ public Map generateI18NForPattern(String pattern) { Map i18n = new HashMap<>(); i18n.put(null, new StringBuffer()); Matcher matcher = Pattern.compile("\\{(.*?)}").matcher(pattern); int lastEnd = 0; while (matcher.find()) { // add the stuff before the match for (StringBuffer sb : i18n.values()) sb.append(pattern, lastEnd, matcher.start()); lastEnd = matcher.end(); // and then the value for the match String defKey = matcher.group(1); // scan all translated tags Map i18nTags = getTagsByPrefix(defKey); for (Map.Entry kv : i18nTags.entrySet()) { if (!kv.getKey().equals(defKey)) { String lang = kv.getKey().substring(defKey.length() + 1); if (!i18n.containsKey(lang)) i18n.put(lang, new StringBuffer(i18n.get(null))); } } // get the simple value (eg: description=...) String defTag = getTag(defKey); if (defTag == null && !i18nTags.isEmpty()) { defTag = i18nTags.values().iterator().next(); } // get the translated value, if exists for (String lang : i18n.keySet()) { String i18nTag = getTag(defKey + ":" + lang); i18n.get(lang).append(i18nTag != null ? i18nTag : (defTag != null ? defTag : "")); } } for (StringBuffer sb : i18n.values()) sb.append(pattern, lastEnd, pattern.length()); Map out = new HashMap<>(i18n.size()); for (Map.Entry kv : i18n.entrySet()) out.put( kv.getKey(), kv.getValue().toString() ); return out; } private Map getTagsByPrefix(String prefix) { Map out = new HashMap<>(); for (Map.Entry entry : tags.entrySet()) { String k = entry.getKey(); if (k.equals(prefix) || k.startsWith(prefix + ":")) { out.put(k, entry.getValue()); } } return out; } /** * Returns true if this element is under construction. */ public boolean isUnderConstruction() { String highway = getTag("highway"); String cycleway = getTag("cycleway"); return "construction".equals(highway) || "construction".equals(cycleway); } /** * Returns true if access is generally denied to this element (potentially with exceptions). */ public boolean isGeneralAccessDenied() { return isTagDeniedAccess("access"); } /** * Returns true if cars are explicitly denied access. */ public boolean isMotorcarExplicitlyDenied() { return isTagDeniedAccess("motorcar"); } /** * Returns true if cars are explicitly allowed. */ public boolean isMotorcarExplicitlyAllowed() { return doesTagAllowAccess("motorcar"); } /** * Returns true if cars/motorcycles/HGV are explicitly denied access. */ public boolean isMotorVehicleExplicitlyDenied() { return isTagDeniedAccess("motor_vehicle"); } /** * Returns true if cars/motorcycles/HGV are explicitly allowed. */ public boolean isMotorVehicleExplicitlyAllowed() { return doesTagAllowAccess("motor_vehicle"); } /** * Returns true if all land vehicles (including bicycles) are explicitly denied access. */ public boolean isVehicleExplicitlyDenied() { return isTagDeniedAccess("vehicle"); } /** * Returns true if all land vehicles (including bicycles) are explicitly allowed. */ public boolean isVehicleExplicitlyAllowed() { return doesTagAllowAccess("vehicle"); } /** * Returns true if bikes are explicitly denied access. *

* bicycle is denied if bicycle:no, bicycle:dismount or bicycle:license. */ public boolean isBicycleExplicitlyDenied() { return (isTagDeniedAccess("bicycle") || "dismount".equals(getTag("bicycle"))); } /** * Returns true if bikes are explicitly allowed. */ public boolean isBicycleExplicitlyAllowed() { return doesTagAllowAccess("bicycle"); } /** * Returns true if pedestrians are explicitly denied access. */ public boolean isPedestrianExplicitlyDenied() { return isTagDeniedAccess("foot"); } /** * Returns true if pedestrians are explicitly allowed. */ public boolean isPedestrianExplicitlyAllowed() { return doesTagAllowAccess("foot"); } /** * @return True if this node / area is a parking. */ public boolean isParking() { return isTag("amenity", "parking"); } /** * @return True if this node / area is a park and ride. */ public boolean isParkAndRide() { String parkingType = getTag("parking"); String parkAndRide = getTag("park_ride"); return ( isParking() && ((parkingType != null && parkingType.contains("park_and_ride")) || (parkAndRide != null && !parkAndRide.equalsIgnoreCase("no"))) ); } /** * Is this a public transport boarding location where passengers wait for transit and that can be * linked to a transit stop vertex later on. *

* This intentionally excludes railway=stop and public_transport=stop because these are supposed * to be placed on the tracks not on the platform. * * @return whether the node is a place used to board a public transport vehicle */ public boolean isBoardingLocation() { return ( isTag("highway", "bus_stop") || isTag("railway", "tram_stop") || isTag("railway", "station") || isTag("railway", "halt") || isTag("amenity", "bus_station") || isTag("amenity", "ferry_terminal") || isTag("highway", "platform") || isPlatform() ); } /** * Determines if an entity is a platform. *

* However, they are filtered out if they are tagged usage=tourism. This prevents miniature tourist * railways like the one in Portland's Zoo (https://www.openstreetmap.org/way/119108622) * from being linked to transit stops that are underneath it. **/ public boolean isPlatform() { var isPlatform = isTag("public_transport", "platform") || isRailwayPlatform(); return isPlatform && !isTag("usage", "tourism"); } public boolean isRailwayPlatform() { return isTag("railway", "platform"); } /** * @return True if this entity provides an entrance to a platform or similar entity */ public boolean isEntrance() { return ( (isTag("railway", "subway_entrance") || isTag("highway", "elevator") || isTag("entrance", "yes") || isTag("entrance", "main")) && !isTag("access", "private") && !isTag("access", "no") ); } /** * @return True if this node / area is a bike parking. */ public boolean isBikeParking() { return ( isTag("amenity", "bicycle_parking") && !isTag("access", "private") && !isTag("access", "no") ); } public void setCreativeName(I18NString creativeName) { this.creativeName = creativeName; } @Nullable public String url() { return null; } /** * Returns all non-empty values of the tags passed in as input values. *

* Values are split by semicolons. */ public Set getMultiTagValues(Set refTags) { return refTags .stream() .map(this::getTag) .filter(Objects::nonNull) .flatMap(v -> Arrays.stream(v.split(";"))) .map(String::strip) .filter(v -> !v.isBlank()) .collect(Collectors.toUnmodifiableSet()); } public OsmProvider getOsmProvider() { return osmProvider; } public void setOsmProvider(OsmProvider provider) { this.osmProvider = provider; } /** * 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. */ public boolean isRoutable() { if (isOneOfTags("highway", NON_ROUTABLE_HIGHWAYS)) { return false; } else if (hasTag("highway") || isPlatform() || isIndoorRoutable()) { if (isGeneralAccessDenied()) { // There are exceptions. return ( isMotorcarExplicitlyAllowed() || isBicycleExplicitlyAllowed() || isPedestrianExplicitlyAllowed() || isMotorVehicleExplicitlyAllowed() || isVehicleExplicitlyAllowed() ); } return true; } return false; } public boolean isIndoorRoutable() { return isOneOfTags("indoor", INDOOR_ROUTABLE_VALUES); } /** * Is this a link to another road, like a highway ramp. */ public boolean isLink() { String highway = getTag("highway"); return highway != null && highway.endsWith(("_link")); } public boolean isElevator() { return isTag("highway", "elevator"); } /** * @return true if there is no explicit tag that makes this unsuitable for wheelchair use. * In other words: we assume that something is wheelchair-accessible in the absence * of other information. */ public boolean isWheelchairAccessible() { return !isTagFalse("wheelchair"); } /** * Does this entity have tags that allow extracting a name? */ public boolean isNamed() { return hasTag("name") || hasTag("ref"); } /** * Is this entity unnamed? *

* Perhaps this entity has a name that isn't in the source data, but it's also possible that * it's explicitly tagged as not having one. * * @see OsmEntity#isExplicitlyUnnamed() */ public boolean hasNoName() { return !isNamed(); } /** * Whether this entity explicitly doesn't have a name. This is different to no name being * set on the entity in OSM. * * @see OsmEntity#isNamed() * @see https://wiki.openstreetmap.org/wiki/Tag:noname%3Dyes */ public boolean isExplicitlyUnnamed() { return isTagTrue("noname"); } /** * Returns true if this tag is explicitly access to this entity. */ private boolean isTagDeniedAccess(String tagName) { String tagValue = getTag(tagName); return "no".equals(tagValue) || "license".equals(tagValue); } /** * Returns level tag (i.e. building floor) or layer tag values, defaults to "0" * Some entities can have a semicolon separated list of levels (e.g. elevators) */ public Set getLevels() { var levels = getMultiTagValues(LEVEL_TAGS); if (levels.isEmpty()) { // default return DEFAULT_LEVEL; } return levels; } /** * Given an assumed traversal permissions, check if there are explicit additional tags, like bicycle=no * or bicycle=yes that override them. */ public StreetTraversalPermission overridePermissions(StreetTraversalPermission def) { StreetTraversalPermission permission; /* * 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 (isGeneralAccessDenied()) { // this can actually be overridden permission = StreetTraversalPermission.NONE; } else { permission = def; } if (isVehicleExplicitlyDenied()) { permission = permission.remove(StreetTraversalPermission.BICYCLE_AND_CAR); } else if (isVehicleExplicitlyAllowed()) { permission = permission.add(StreetTraversalPermission.BICYCLE_AND_CAR); } if (isMotorcarExplicitlyDenied() || isMotorVehicleExplicitlyDenied()) { permission = permission.remove(StreetTraversalPermission.CAR); } else if (isMotorcarExplicitlyAllowed() || isMotorVehicleExplicitlyAllowed()) { permission = permission.add(StreetTraversalPermission.CAR); } if (isBicycleExplicitlyDenied()) { permission = permission.remove(StreetTraversalPermission.BICYCLE); } else if (isBicycleExplicitlyAllowed()) { permission = permission.add(StreetTraversalPermission.BICYCLE); } if (isPedestrianExplicitlyDenied()) { permission = permission.remove(StreetTraversalPermission.PEDESTRIAN); } else if (isPedestrianExplicitlyAllowed()) { permission = permission.add(StreetTraversalPermission.PEDESTRIAN); } if (isUnderConstruction()) { permission = StreetTraversalPermission.NONE; } if (permission == null) { return 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 bike permissions, check consistency. if (isBicycleExplicitlyAllowed()) { permission = permission.add(StreetTraversalPermission.BICYCLE); } if (isBicycleDismountForced()) { permission = permission.remove(StreetTraversalPermission.BICYCLE); } return permission; } @Override public String toString() { return ToStringBuilder.of(this.getClass()).addObj("tags", tags).toString(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy