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

org.opentripplanner.osm.wayproperty.WayPropertySet Maven / Gradle / Ivy

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

import static org.opentripplanner.osm.wayproperty.WayPropertiesBuilder.withModes;
import static org.opentripplanner.street.model.StreetTraversalPermission.ALL;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.opentripplanner.framework.functional.FunctionUtils.TriFunction;
import org.opentripplanner.framework.i18n.I18NString;
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
import org.opentripplanner.osm.model.OsmEntity;
import org.opentripplanner.osm.wayproperty.specifier.BestMatchSpecifier;
import org.opentripplanner.osm.wayproperty.specifier.OsmSpecifier;
import org.opentripplanner.street.model.StreetTraversalPermission;
import org.opentripplanner.street.model.note.StreetNoteAndMatcher;
import org.opentripplanner.street.model.note.StreetNoteMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Information given to the GraphBuilder about how to assign permissions, safety values, names, etc.
 * to edges based on OSM tags.
 * 

* WayPropertyPickers, CreativeNamePickers, SlopeOverridePickers, and SpeedPickers are applied to ways based on how well * their OSMSpecifiers match a given OSM way. Generally one OSMSpecifier will win out over all the others based on the * number of exact, partial, and wildcard tag matches. See OSMSpecifier for more details on the matching process. */ public class WayPropertySet { private static final Logger LOG = LoggerFactory.getLogger(WayPropertySet.class); /** Sets 1.0 as default safety value for all permissions. */ private final TriFunction< StreetTraversalPermission, Float, OsmEntity, Double > DEFAULT_SAFETY_RESOLVER = ((permission, speedLimit, osmWay) -> 1.0); private final List wayProperties; /** Assign names to ways that do not have them based on OSM tags. */ private final List creativeNamers; private final List slopeOverrides; /** Assign automobile speeds based on OSM tags. */ private final List speedPickers; private final List notes; private final Pattern maxSpeedPattern; /** The automobile speed for street segments that do not match any SpeedPicker. */ public Float defaultCarSpeed; /** * The maximum automobile speed that can be defined through OSM speed limit tagging. Car speed * defaults for different way types can be higher than this. */ public Float maxPossibleCarSpeed; /** * The maximum automobile speed that has been used. This can be used in heuristics later on to * determine the minimum travel time. */ public float maxUsedCarSpeed = 0f; /** Resolves walk safety value for each {@link StreetTraversalPermission}. */ private TriFunction< StreetTraversalPermission, Float, OsmEntity, Double > defaultWalkSafetyForPermission; /** Resolves bicycle safety value for each {@link StreetTraversalPermission}. */ private TriFunction< StreetTraversalPermission, Float, OsmEntity, Double > defaultBicycleSafetyForPermission; /** The WayProperties applied to all ways that do not match any WayPropertyPicker. */ private final WayProperties defaultProperties; private final DataImportIssueStore issueStore; public List getMixins() { return mixins; } private final List mixins = new ArrayList<>(); public WayPropertySet() { this(DataImportIssueStore.NOOP); } public WayPropertySet(DataImportIssueStore issueStore) { /* sensible defaults */ // 11.2 m/s ~= 25 mph ~= 40 kph, standard speed limit in the US defaultCarSpeed = 11.2f; // 38 m/s ~= 85 mph ~= 137 kph, max speed limit in the US maxPossibleCarSpeed = 38f; defaultProperties = withModes(ALL).build(); wayProperties = new ArrayList<>(); creativeNamers = new ArrayList<>(); slopeOverrides = new ArrayList<>(); speedPickers = new ArrayList<>(); notes = new ArrayList<>(); // regex courtesy http://wiki.openstreetmap.org/wiki/Key:maxspeed // and edited maxSpeedPattern = Pattern.compile("^([0-9][.0-9]*)\\s*(kmh|km/h|kmph|kph|mph|knots)?$"); defaultWalkSafetyForPermission = DEFAULT_SAFETY_RESOLVER; defaultBicycleSafetyForPermission = DEFAULT_SAFETY_RESOLVER; this.issueStore = issueStore; } /** * Applies the WayProperties whose OSMPicker best matches this way. In addition, WayProperties * that are mixins will have their safety values applied if they match at all. */ public WayProperties getDataForWay(OsmEntity way) { WayProperties backwardResult = defaultProperties; WayProperties forwardResult = defaultProperties; int bestBackwardScore = 0; int bestForwardScore = 0; List backwardMixins = new ArrayList<>(); List forwardMixins = new ArrayList<>(); for (WayPropertyPicker picker : wayProperties) { OsmSpecifier specifier = picker.specifier(); WayProperties wayProperties = picker.properties(); var score = specifier.matchScores(way); if (score.backward() > bestBackwardScore) { backwardResult = wayProperties; bestBackwardScore = score.backward(); } if (score.forward() > bestForwardScore) { forwardResult = wayProperties; bestForwardScore = score.forward(); } } for (var mixin : mixins) { var score = mixin.specifier().matchScores(way); if (score.backward() > 0) { backwardMixins.add(mixin); } if (score.forward() > 0) { forwardMixins.add(mixin); } } float forwardSpeed = getCarSpeedForWay(way, false); float backSpeed = getCarSpeedForWay(way, true); var permission = way.overridePermissions(forwardResult.getPermission()); var backwardPermission = way.overridePermissions(backwardResult.getPermission()); WayProperties result = forwardResult .mutate() .withPermission(permission) .bicycleSafety( forwardResult .bicycleSafetyOpt() .map(SafetyFeatures::forward) .orElseGet(() -> defaultBicycleSafetyForPermission.apply(permission, forwardSpeed, way)), backwardResult .bicycleSafetyOpt() .map(SafetyFeatures::back) .orElseGet(() -> defaultBicycleSafetyForPermission.apply(backwardPermission, backSpeed, way) ) ) .walkSafety( forwardResult .walkSafetyOpt() .map(SafetyFeatures::forward) .orElseGet(() -> defaultWalkSafetyForPermission.apply(permission, forwardSpeed, way)), backwardResult .walkSafetyOpt() .map(SafetyFeatures::back) .orElseGet(() -> defaultWalkSafetyForPermission.apply(backwardPermission, backSpeed, way)) ) .build(); /* apply mixins */ if (!backwardMixins.isEmpty()) { result = applyMixins(result, backwardMixins, true); } if (!forwardMixins.isEmpty()) { result = applyMixins(result, forwardMixins, false); } if ( (bestBackwardScore == 0 || bestForwardScore == 0) && (backwardMixins.isEmpty() || forwardMixins.isEmpty()) ) { String all_tags = dumpTags(way); LOG.debug("Used default permissions: {}", all_tags); } return result; } public I18NString getCreativeNameForWay(OsmEntity way) { CreativeNamer bestNamer = null; int bestScore = 0; for (CreativeNamerPicker picker : creativeNamers) { OsmSpecifier specifier = picker.specifier; CreativeNamer namer = picker.namer; int score = specifier.matchScore(way); if (score > bestScore) { bestNamer = namer; bestScore = score; } } if (bestNamer == null) { return null; } return bestNamer.generateCreativeName(way); } /** * Calculate the automobile speed, in meters per second, for this way. */ public float getCarSpeedForWay(OsmEntity way, boolean backward) { // first, check for maxspeed tags Float speed = null; Float currentSpeed; if (way.hasTag("maxspeed:motorcar")) { speed = getMetersSecondFromSpeed(way.getTag("maxspeed:motorcar")); } if (speed == null && !backward && way.hasTag("maxspeed:forward")) { speed = getMetersSecondFromSpeed(way.getTag("maxspeed:forward")); } if (speed == null && backward && way.hasTag("maxspeed:backward")) { speed = getMetersSecondFromSpeed(way.getTag("maxspeed:backward")); } if (speed == null && way.hasTag("maxspeed:lanes")) { for (String lane : way.getTag("maxspeed:lanes").split("\\|")) { currentSpeed = getMetersSecondFromSpeed(lane); // Pick the largest speed from the tag // currentSpeed might be null if it was invalid, for instance 10|fast|20 if (currentSpeed != null && (speed == null || currentSpeed > speed)) { speed = currentSpeed; } } } if (way.hasTag("maxspeed") && speed == null) speed = getMetersSecondFromSpeed( way.getTag("maxspeed") ); if (speed != null) { // Too low (less than 5 km/h) or too high speed limit indicates an error in the data, // we use default speed limits for the way type in that case. // The small epsilon is to account for possible rounding errors. if (speed < 1.387 || speed > maxPossibleCarSpeed + 0.0001) { var id = way.getId(); var link = way.url(); issueStore.add( "InvalidCarSpeedLimit", "OSM object with id '%s' (%s) has an invalid maxspeed value (%f), that speed will be ignored", id, link, speed ); } else { if (speed > maxUsedCarSpeed) { maxUsedCarSpeed = speed; } return speed; } } // otherwise, we use the speedPickers int bestScore = 0; Float bestSpeed = null; int score; // SpeedPickers are constructed in DefaultOsmTagMapper with an OSM specifier // (e.g. highway=motorway) and a default speed for that segment. for (SpeedPicker picker : speedPickers) { OsmSpecifier specifier = picker.specifier; score = specifier.matchScore(way); if (score > bestScore) { bestScore = score; bestSpeed = picker.speed; } } if (bestSpeed != null) { if (bestSpeed > maxUsedCarSpeed) { maxUsedCarSpeed = bestSpeed; } return bestSpeed; } else { return this.defaultCarSpeed; } } public Set getNoteForWay(OsmEntity way) { HashSet out = new HashSet<>(); for (NotePicker picker : notes) { OsmSpecifier specifier = picker.specifier; NoteProperties noteProperties = picker.noteProperties; if (specifier.matchScore(way) > 0) { out.add(noteProperties.generateNote(way)); } } return out; } public boolean getSlopeOverride(OsmEntity way) { boolean result = false; int bestScore = 0; for (SlopeOverridePicker picker : slopeOverrides) { OsmSpecifier specifier = picker.getSpecifier(); int score = specifier.matchScore(way); if (score > bestScore) { result = picker.getOverride(); bestScore = score; } } return result; } public void addMixin(MixinProperties mixin) { mixins.add(mixin); } public void addProperties(OsmSpecifier spec, WayProperties properties) { wayProperties.add(new WayPropertyPicker(spec, properties)); } public void addCreativeNamer(OsmSpecifier spec, CreativeNamer namer) { creativeNamers.add(new CreativeNamerPicker(spec, namer)); } public void addNote(OsmSpecifier osmSpecifier, NoteProperties properties) { notes.add(new NotePicker(osmSpecifier, properties)); } public void setSlopeOverride(OsmSpecifier spec, boolean override) { slopeOverrides.add(new SlopeOverridePicker(spec, override)); } public int hashCode() { return ( defaultProperties.hashCode() + wayProperties.hashCode() + creativeNamers.hashCode() + slopeOverrides.hashCode() ); } public boolean equals(Object o) { if (o instanceof WayPropertySet other) { return ( defaultProperties.equals(other.defaultProperties) && wayProperties.equals(other.wayProperties) && creativeNamers.equals(other.creativeNamers) && slopeOverrides.equals(other.slopeOverrides) && notes.equals(other.notes) ); } return false; } public void addSpeedPicker(SpeedPicker picker) { this.speedPickers.add(picker); } public Float getMetersSecondFromSpeed(String speed) { Matcher m = maxSpeedPattern.matcher(speed.trim()); if (!m.matches()) { return null; } float originalUnits; try { originalUnits = (float) Double.parseDouble(m.group(1)); } catch (NumberFormatException e) { LOG.warn("Could not parse max speed {}", m.group(1)); return null; } String units = m.group(2); if (units == null || units.isEmpty()) units = "kmh"; // we'll be doing quite a few string comparisons here units = units.intern(); float metersSecond; switch (units) { case "kmh": case "km/h": case "kmph": case "kph": metersSecond = 0.277778f * originalUnits; break; case "mph": metersSecond = 0.446944f * originalUnits; break; case "knots": metersSecond = 0.514444f * originalUnits; break; default: return null; } return metersSecond; } public void createNames(String spec, String patternKey) { CreativeNamer namer = new CreativeNamer(patternKey); addCreativeNamer(new BestMatchSpecifier(spec), namer); } public void createNotes(String spec, String patternKey, StreetNoteMatcher matcher) { // TODO: notes aren't localized NoteProperties properties = new NoteProperties(patternKey, matcher); addNote(new BestMatchSpecifier(spec), properties); } /** * A custom defaultWalkSafetyForPermission can only be set once. The given function should * provide a default for each permission. Safety can vary based on car speed limit on a way. */ public void setDefaultWalkSafetyForPermission( TriFunction defaultWalkSafetyForPermission ) { if (!this.defaultWalkSafetyForPermission.equals(DEFAULT_SAFETY_RESOLVER)) { throw new IllegalStateException("A custom default walk safety resolver was already set"); } this.defaultWalkSafetyForPermission = defaultWalkSafetyForPermission; } /** * A custom defaultBicycleSafetyForPermission can only be set once. The given function should * provide a default for each permission. Safety can vary based on car speed limit on a way. */ public void setDefaultBicycleSafetyForPermission( TriFunction< StreetTraversalPermission, Float, OsmEntity, Double > defaultBicycleSafetyForPermission ) { if (!this.defaultBicycleSafetyForPermission.equals(DEFAULT_SAFETY_RESOLVER)) { throw new IllegalStateException("A custom default cycling safety resolver was already set"); } this.defaultBicycleSafetyForPermission = defaultBicycleSafetyForPermission; } public void setMixinProperties(OsmSpecifier spec, MixinPropertiesBuilder builder) { addMixin(builder.build(spec)); } public void setMixinProperties(String spec, MixinPropertiesBuilder builder) { setMixinProperties(new BestMatchSpecifier(spec), builder); } public void setProperties(String s, WayProperties props) { setProperties(new BestMatchSpecifier(s), props); } public void setProperties(String spec, WayPropertiesBuilder properties) { setProperties(new BestMatchSpecifier(spec), properties); } public void setProperties(OsmSpecifier spec, WayProperties properties) { addProperties(spec, properties); } public void setProperties(OsmSpecifier spec, WayPropertiesBuilder properties) { addProperties(spec, properties.build()); } public void setCarSpeed(String spec, float speed) { SpeedPicker picker = new SpeedPicker(); picker.specifier = new BestMatchSpecifier(spec); picker.speed = speed; addSpeedPicker(picker); } public void setCarSpeed(OsmSpecifier spec, float speed) { SpeedPicker picker = new SpeedPicker(); picker.specifier = spec; picker.speed = speed; addSpeedPicker(picker); } public List getWayProperties() { return Collections.unmodifiableList(wayProperties); } private String dumpTags(OsmEntity way) { /* generate warning message */ String all_tags = null; Map tags = way.getTags(); for (Entry entry : tags.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); String tag = key + "=" + value; if (all_tags == null) { all_tags = tag; } else { all_tags += "; " + tag; } } return all_tags; } private WayProperties applyMixins( WayProperties result, List mixins, boolean backward ) { SafetyFeatures bicycleSafetyFeatures = result.bicycleSafety(); double forwardBicycle = bicycleSafetyFeatures.forward(); double backBicycle = bicycleSafetyFeatures.back(); SafetyFeatures walkSafetyFeatures = result.walkSafety(); double forwardWalk = walkSafetyFeatures.forward(); double backWalk = walkSafetyFeatures.back(); for (var mixin : mixins) { if (backward) { if (mixin.bicycleSafety() != null) { backBicycle *= mixin.bicycleSafety().back(); } if (mixin.walkSafety() != null) { backWalk *= mixin.walkSafety().back(); } } else { if (mixin.bicycleSafety() != null) { forwardBicycle *= mixin.bicycleSafety().forward(); } if (mixin.walkSafety() != null) { forwardWalk *= mixin.walkSafety().forward(); } } } return result .mutate() .bicycleSafety(forwardBicycle, backBicycle) .walkSafety(forwardWalk, backWalk) .build(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy