org.opentripplanner.graph_builder.module.osm.WayPropertySet Maven / Gradle / Ivy
package org.opentripplanner.graph_builder.module.osm;
import static org.opentripplanner.graph_builder.module.osm.WayPropertiesBuilder.withModes;
import static org.opentripplanner.routing.edgetype.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.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.opentripplanner.common.model.T2;
import org.opentripplanner.graph_builder.module.osm.specifier.BestMatchSpecifier;
import org.opentripplanner.graph_builder.module.osm.specifier.OsmSpecifier;
import org.opentripplanner.model.StreetNote;
import org.opentripplanner.openstreetmap.model.OSMWithTags;
import org.opentripplanner.routing.edgetype.StreetTraversalPermission;
import org.opentripplanner.routing.services.notes.NoteMatcher;
import org.opentripplanner.transit.model.basic.I18NString;
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.
* TODO rename so that the connection with OSM tags is obvious
*
* 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 BiFunction DEFAULT_SAFETY_RESOLVER =
((permission, speedLimit) -> 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 defaultSpeed;
/** Resolves walk safety value for each {@link StreetTraversalPermission}. */
private BiFunction defaultWalkSafetyForPermission;
/** Resolves bicycle safety value for each {@link StreetTraversalPermission}. */
private BiFunction defaultBicycleSafetyForPermission;
/** The WayProperties applied to all ways that do not match any WayPropertyPicker. */
private final WayProperties defaultProperties;
public WayPropertySet() {
/* sensible defaults */
defaultSpeed = 11.2f; // 11.2 m/s ~= 25 mph ~= 40 kph, standard speed limit in the US
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;
}
/**
* 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(OSMWithTags way) {
WayProperties leftResult = defaultProperties;
WayProperties rightResult = defaultProperties;
int bestLeftScore = 0;
int bestRightScore = 0;
List leftMixins = new ArrayList<>();
List rightMixins = new ArrayList<>();
for (WayPropertyPicker picker : wayProperties) {
OsmSpecifier specifier = picker.specifier();
WayProperties wayProperties = picker.properties();
var score = specifier.matchScores(way);
if (picker.safetyMixin()) {
if (score.left() > 0) {
leftMixins.add(wayProperties);
}
if (score.right() > 0) {
rightMixins.add(wayProperties);
}
} else {
if (score.left() > bestLeftScore) {
leftResult = wayProperties;
bestLeftScore = score.left();
}
if (score.right() > bestRightScore) {
rightResult = wayProperties;
bestRightScore = score.right();
}
}
}
float forwardSpeed = getCarSpeedForWay(way, false);
float backSpeed = getCarSpeedForWay(way, true);
StreetTraversalPermission permission = rightResult.getPermission();
WayProperties result = rightResult
.mutate()
.bicycleSafety(
rightResult.getBicycleSafetyFeatures() != null
? rightResult.getBicycleSafetyFeatures().forward()
: defaultBicycleSafetyForPermission.apply(permission, forwardSpeed),
leftResult.getBicycleSafetyFeatures() != null
? leftResult.getBicycleSafetyFeatures().back()
: defaultBicycleSafetyForPermission.apply(permission, backSpeed)
)
.walkSafety(
rightResult.getWalkSafetyFeatures() != null
? rightResult.getWalkSafetyFeatures().forward()
: defaultWalkSafetyForPermission.apply(permission, forwardSpeed),
leftResult.getWalkSafetyFeatures() != null
? leftResult.getWalkSafetyFeatures().back()
: defaultWalkSafetyForPermission.apply(permission, backSpeed)
)
.build();
/* apply mixins */
if (leftMixins.size() > 0) {
result = applyMixins(result, leftMixins, false);
}
if (rightMixins.size() > 0) {
result = applyMixins(result, rightMixins, true);
}
if (
(bestLeftScore == 0 || bestRightScore == 0) &&
(leftMixins.size() == 0 || rightMixins.size() == 0)
) {
String all_tags = dumpTags(way);
LOG.debug("Used default permissions: {}", all_tags);
}
return result;
}
public I18NString getCreativeNameForWay(OSMWithTags 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(OSMWithTags way, boolean back) {
// first, check for maxspeed tags
Float speed = null;
Float currentSpeed;
if (way.hasTag("maxspeed:motorcar")) speed =
getMetersSecondFromSpeed(way.getTag("maxspeed:motorcar"));
if (speed == null && !back && way.hasTag("maxspeed:forward")) speed =
getMetersSecondFromSpeed(way.getTag("maxspeed:forward"));
if (speed == null && back && way.hasTag("maxspeed:reverse")) speed =
getMetersSecondFromSpeed(way.getTag("maxspeed:reverse"));
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"));
// this would be bad, as the segment could never be traversed by an automobile
// The small epsilon is to account for possible rounding errors
if (speed != null && speed < 0.0001) LOG.warn(
"Zero or negative automobile speed detected at {} based on OSM " +
"maxspeed tags; ignoring these tags",
this
);
// if there was a defined speed and it's not 0, we're done
if (speed != null && speed > 0.0001) 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) {
return bestSpeed;
} else {
return this.defaultSpeed;
}
}
public Set> getNoteForWay(OSMWithTags 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));
}
}
if (out.size() == 0) {
return null;
}
return out;
}
public boolean getSlopeOverride(OSMWithTags 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 addProperties(OsmSpecifier spec, WayProperties properties, boolean mixin) {
wayProperties.add(new WayPropertyPicker(spec, properties, mixin));
}
public void addProperties(OsmSpecifier spec, WayProperties properties) {
wayProperties.add(new WayPropertyPicker(spec, properties, false));
}
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.equals("")) 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, NoteMatcher 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(
BiFunction 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(
BiFunction 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, WayPropertiesBuilder properties) {
addProperties(spec, properties.build(), true);
}
public void setMixinProperties(String spec, WayPropertiesBuilder properties) {
setMixinProperties(spec, properties.build());
}
public void setMixinProperties(String spec, WayProperties properties) {
addProperties(new BestMatchSpecifier(spec), properties, true);
}
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, false);
}
public void setProperties(OsmSpecifier spec, WayPropertiesBuilder properties) {
addProperties(spec, properties.build(), false);
}
public void setCarSpeed(String spec, float speed) {
SpeedPicker picker = new SpeedPicker();
picker.specifier = new BestMatchSpecifier(spec);
picker.speed = speed;
addSpeedPicker(picker);
}
public List getWayProperties() {
return Collections.unmodifiableList(wayProperties);
}
private String dumpTags(OSMWithTags 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 right
) {
SafetyFeatures bicycleSafetyFeatures = result.getBicycleSafetyFeatures();
double forwardBicycle = bicycleSafetyFeatures.forward();
double backBicycle = bicycleSafetyFeatures.back();
SafetyFeatures walkSafetyFeatures = result.getWalkSafetyFeatures();
double forwardWalk = walkSafetyFeatures.forward();
double backWalk = walkSafetyFeatures.back();
for (WayProperties properties : mixins) {
if (right) {
if (properties.getBicycleSafetyFeatures() != null) {
backBicycle *= properties.getBicycleSafetyFeatures().back();
}
if (properties.getWalkSafetyFeatures() != null) {
backWalk *= properties.getWalkSafetyFeatures().back();
}
} else {
if (properties.getBicycleSafetyFeatures() != null) {
forwardBicycle *= properties.getBicycleSafetyFeatures().forward();
}
if (properties.getWalkSafetyFeatures() != null) {
forwardWalk *= properties.getWalkSafetyFeatures().forward();
}
}
}
return result
.mutate()
.bicycleSafety(forwardBicycle, backBicycle)
.walkSafety(forwardWalk, backWalk)
.build();
}
}