WheelchairGraphStorageBuilder.java
/* This file is part of Openrouteservice.
*
* Openrouteservice is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
* This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public License along with this library;
* if not, see <https://www.gnu.org/licenses/>.
*/
package org.heigit.ors.routing.graphhopper.extensions.storages.builders;
import com.graphhopper.GraphHopper;
import com.graphhopper.reader.ReaderWay;
import com.graphhopper.storage.GraphExtension;
import com.graphhopper.util.EdgeIteratorState;
import org.heigit.ors.routing.graphhopper.extensions.WheelchairAttributes;
import org.heigit.ors.routing.graphhopper.extensions.WheelchairTypesEncoder;
import org.heigit.ors.routing.graphhopper.extensions.storages.WheelchairAttributesGraphStorage;
import org.heigit.ors.util.UnitsConverter;
import org.locationtech.jts.geom.Coordinate;
import java.util.*;
public class WheelchairGraphStorageBuilder extends AbstractGraphStorageBuilder {
public static final String KEY_SLOPED_CURB = "sloped_curb";
public static final String KEY_SLOPED_KERB = "sloped_kerb";
public static final String KEY_KERB_HEIGHT = "kerb:height";
public static final String KEY_FOOTWAY = "footway";
public static final String SW_VAL_RIGHT = "right";
public static final String SW_VAL_LEFT = "left";
public static final String KEY_BOTH = "both";
public static final String KEY_SIDEWALK_BOTH = "sidewalk:both:";
public static final String KEY_FOOTWAY_BOTH = "footway:both:";
public static final String KEY_CURB_HEIGHT = "curb:height";
public enum Side {
LEFT,
RIGHT,
NONE
}
private WheelchairAttributesGraphStorage storage;
private final WheelchairAttributes wheelchairAttributes;
private final WheelchairAttributes wheelchairAttributesLeftSide;
private final WheelchairAttributes wheelchairAttributesRightSide;
private Map<Integer, Map<String, String>> nodeTagsOnWay;
private Map<String, Object> cleanedTags;
private boolean hasLeftSidewalk = false;
private boolean hasRightSidewalk = false;
private boolean kerbHeightOnlyOnCrossing = false;
public WheelchairGraphStorageBuilder() {
wheelchairAttributes = new WheelchairAttributes();
wheelchairAttributesLeftSide = new WheelchairAttributes();
wheelchairAttributesRightSide = new WheelchairAttributes();
nodeTagsOnWay = new HashMap<>();
cleanedTags = new HashMap<>();
}
/**
* Constructor - Used for testing
*
* @param onlyAttachKerbsToCrossings Only attach kerb heights to crossings?
*/
public WheelchairGraphStorageBuilder(boolean onlyAttachKerbsToCrossings) {
this();
kerbHeightOnlyOnCrossing = onlyAttachKerbsToCrossings;
}
/**
* Initiate the wheelchair storage builder
*
* @param graphhopper The graphhopper instance to run against
* @throws Exception Thrown when the storage has already been initialized
* @return The storage that is created from the builder
*/
@Override
public GraphExtension init(GraphHopper graphhopper) throws Exception {
if (storage != null)
throw new Exception("GraphStorageBuilder has been already initialized.");
if (parameters.containsKey("KerbsOnCrossings")) {
kerbHeightOnlyOnCrossing = Boolean.parseBoolean(parameters.get("KerbsOnCrossings"));
}
storage = new WheelchairAttributesGraphStorage();
return storage;
}
/**
* Call the processWay method with empty coordinates and tags
*
* @param way The way to process
*/
@Override
public void processWay(ReaderWay way) {
this.processWay(way, new Coordinate[0], new HashMap<>());
}
/**
* Process the way
*
* @param way The way to be processed
* @param coords Coordinates of the way
* @param nodeTags Tags that have been stored on nodes of the way that should be used during processing
*/
@Override
public void processWay(ReaderWay way, Coordinate[] coords, Map<Integer, Map<String, String>> nodeTags) {
// Start by resetting storage variables after the previous way
wheelchairAttributes.reset();
wheelchairAttributesLeftSide.reset();
wheelchairAttributesRightSide.reset();
hasRightSidewalk = false;
hasLeftSidewalk = false;
this.nodeTagsOnWay = nodeTags;
// Annoyingly, it seems often to be the case that rather than using ":" to seperate tag parts, "." is used, so
// we need to take this into account
cleanedTags = cleanTags(way.getTags());
// Now we need to process the way specific to whether it is a separate feature (i.e. footway) or is attached
// to a road feature (i.e. with the tag sidewalk=left)
processWayCheckForSeparateFeature(way);
// We still need to always process the way itself even if it separate so that we can get sidewalk info (a
// separate footway can still have sidewalk tags...)
processSidewalksAttachedToWay(way);
// the way has known suitability if it can be classified as seperate footway
wheelchairAttributes.setSuitable(isSeparateFootway(way) || way.hasTag("wheelchair_accessible", true));
// the sidewalks always imply known suitability
wheelchairAttributesLeftSide.setSuitable(true);
wheelchairAttributesRightSide.setSuitable(true);
// Process the kerb tags.
processKerbTags();
}
/**
* Return the attributes for the sidewalk on the specified side of the road, or the general attributes when NONE is provided
*
* @param side The side of the road you want the data for
* @return The WheelchairAttributes object containing the inofrmation for the specified side
*/
public WheelchairAttributes getStoredAttributes(Side side) {
return switch (side) {
case LEFT -> wheelchairAttributesLeftSide;
case RIGHT -> wheelchairAttributesRightSide;
case NONE -> wheelchairAttributes;
default -> null;
};
}
/**
* Go through tags and attempt to remove any invalid keys (i.e. when compound keys have been entered using a '.' rather than ':'
*
* @param dirtyTags The OSM tag collection that needs to be cleaned
* @return A cleaned version of the tags on the way (. replaced with : in tag names)
*/
private HashMap<String, Object> cleanTags(Map<String, Object> dirtyTags) {
HashMap<String, Object> cleanedTagsMap = new HashMap<>();
for (Map.Entry<String, Object> entry : dirtyTags.entrySet()) {
String cleanKey = entry.getKey().replace(".", ":");
cleanedTagsMap.put(cleanKey, entry.getValue());
}
return cleanedTagsMap;
}
/**
* Process footways that are attached to an OSM way via the sidewalk tags. It looks for parameters important for
* wheelchair routing such as width, smoothness and kerb height and then stores these in the attributes object
* ready for use when the edge(s) are processed. It also detects which side of the base way that the sidewalks
* have been created for and stores the information appropriately.
*
* @param way The way to be processed
*/
private void processSidewalksAttachedToWay(ReaderWay way) {
detectAndRecordSidewalkSide(way);
// get surface type (asphalt, sand etc.)
setSidewalkAttribute(WheelchairAttributes.Attribute.SURFACE);
// get smoothness value (good, terrible etc.)
setSidewalkAttribute(WheelchairAttributes.Attribute.SMOOTHNESS);
// Get the track type (grade1, grade4 etc.)
setSidewalkAttribute(WheelchairAttributes.Attribute.TRACK);
// Get the width of the way (2, 0.1 etc.)
setSidewalkAttribute(WheelchairAttributes.Attribute.WIDTH);
// Get the incline of the way (10%, 6% etc.)
setSidewalkAttribute(WheelchairAttributes.Attribute.INCLINE);
}
/**
* Process a footway that has been stored in OSM as a separate feature, such as a crossing, footpath or pedestrian
* way. The same as the attached processing, it looks for the different attributes as tags that are important for
* wheelchair routing and stores them against the generic wheelchair storage object
*/
private void processWayCheckForSeparateFeature(ReaderWay way) {
boolean markSurfaceQualityKnown = isSeparateFootway(way);
setWayAttribute(WheelchairAttributes.Attribute.SURFACE, markSurfaceQualityKnown);
setWayAttribute(WheelchairAttributes.Attribute.SMOOTHNESS, markSurfaceQualityKnown);
setWayAttribute(WheelchairAttributes.Attribute.TRACK, markSurfaceQualityKnown);
setWayAttribute(WheelchairAttributes.Attribute.WIDTH, markSurfaceQualityKnown);
setWayAttribute(WheelchairAttributes.Attribute.INCLINE, markSurfaceQualityKnown);
}
/**
* Set the specified attribute in the attribute storage object based on the information gathered from the way. This
* method ony sets the attribute in the attribute storage object for the standalone way and not sidewalks.
*
* @param attribute The attribute to process
* @param markSurfaceQualityKnown Whether or not to also set the surfaceQualityKnown flag in the WheelchairAttributes object
*/
private void setWayAttribute(WheelchairAttributes.Attribute attribute, boolean markSurfaceQualityKnown) {
if (cleanedTags.containsKey(attributeToTagName(attribute))) {
setWheelchairAttribute((String) cleanedTags.get(attributeToTagName(attribute)), attribute, markSurfaceQualityKnown);
}
}
/**
* Set teh specified attribute in the attribute storage objects for the left and right sidewalks,
*
* @param attribute The attribute to process
*/
private void setSidewalkAttribute(WheelchairAttributes.Attribute attribute) {
String[] tagValues;
tagValues = getSidedTagValue(attributeToTagName(attribute));
if (tagValues[0] != null && !tagValues[0].isEmpty()) {
setSidewalkAttributeForSide(tagValues[0], attribute, Side.LEFT);
}
if (tagValues[1] != null && !tagValues[1].isEmpty()) {
setSidewalkAttributeForSide(tagValues[1], attribute, Side.RIGHT);
}
}
/**
* Detect if there are sidewalks stored on the way and if so, mark that these are present
*
* @param way The way to look for sidewalks on
*/
private void detectAndRecordSidewalkSide(ReaderWay way) {
if (way.hasTag("sidewalk")) {
String sw = way.getTag("sidewalk");
switch (sw) {
case SW_VAL_LEFT -> hasLeftSidewalk = true;
case SW_VAL_RIGHT -> hasRightSidewalk = true;
case KEY_BOTH -> {
hasLeftSidewalk = true;
hasRightSidewalk = true;
}
default -> {
}
}
}
}
/**
* Convert an attribute from the wheelchair attribute storage to a corresponding osm tag key
*
* @param attribute The attribute that the tag key is required for
* @return The OSM tag key that corresponds to the attribute
*/
private String attributeToTagName(WheelchairAttributes.Attribute attribute) {
return switch (attribute) {
case SURFACE -> "surface";
case SMOOTHNESS -> "smoothness";
case TRACK -> "tracktype";
case WIDTH -> "width";
case INCLINE -> "incline";
case KERB -> "kerb";
default -> "";
};
}
/**
* Set the specified attribute of the specified sidewalk to be the value passed
*
* @param value The value to store
* @param attribute The attribute to store the value against
* @param side The sidewalk the attribute is for
*/
private void setSidewalkAttributeForSide(String value, WheelchairAttributes.Attribute attribute, Side side) {
switch (side) {
case LEFT -> {
hasLeftSidewalk = true;
wheelchairAttributesLeftSide.setAttribute(attribute, convertTagValueToEncodedValue(attribute, value), true);
}
case RIGHT -> {
hasRightSidewalk = true;
wheelchairAttributesRightSide.setAttribute(attribute, convertTagValueToEncodedValue(attribute, value), true);
}
default -> {
}
}
}
/**
* Set the specified attribute on the standalone way to be the value passed
*
* @param value The value to store
* @param attribute The attribute to store the value against
* @param markSurfaceQualityKnown Whether or not to also set the surfaceQualityKnown flag in the WheelchairAttributes object
*/
private void setWheelchairAttribute(String value, WheelchairAttributes.Attribute attribute, boolean markSurfaceQualityKnown) {
wheelchairAttributes.setAttribute(attribute, convertTagValueToEncodedValue(attribute, value), markSurfaceQualityKnown);
}
/**
* Transform (if needed) a value into an encoded value using the correct encoder.
*
* @param attribute The attribute the value is for
* @param tagValue The string value stored in the tag
* @return The correctly encoded value
*/
private int convertTagValueToEncodedValue(WheelchairAttributes.Attribute attribute, String tagValue) {
switch (attribute) {
case SMOOTHNESS:
case TRACK:
case SURFACE:
try {
return WheelchairTypesEncoder.getEncodedType(attribute, tagValue.toLowerCase());
} catch (Exception notRecognisedEncodedTypeError) {
return -1;
}
case WIDTH:
return (int) (UnitsConverter.convertOSMDistanceTagToMeters(tagValue.toLowerCase()) * 100);
case INCLINE:
return getInclineFromTagValue(tagValue.toLowerCase());
case KERB:
return convertKerbTagValueToCentimetres(tagValue.toLowerCase());
default:
return -1;
}
}
/**
* Get the kerb height value from a set of tags. The method takes into account different ways of spelling and representing the kerb height and then adds the kerb information
* to the wheelchair attribute objects
*/
private void processKerbTags() {
String[] assumedKerbTags = new String[]{
"curb",
"kerb",
KEY_SLOPED_CURB,
KEY_SLOPED_KERB
};
String[] explicitKerbTags = new String[]{
KEY_KERB_HEIGHT,
KEY_CURB_HEIGHT
};
int height = calcSingleKerbHeightFromTagList(assumedKerbTags, -1);
// Explicit heights overwrite assumed
height = calcSingleKerbHeightFromTagList(explicitKerbTags, height);
if (height > -1) {
wheelchairAttributes.setSlopedKerbHeight(height);
}
// Now for if the values are attached to sides of the way
int[] heights = calcSingleKerbHeightFromSidedTagList(assumedKerbTags, new int[]{-1, -1});
heights = calcSingleKerbHeightFromSidedTagList(explicitKerbTags, heights);
if (heights[0] > -1) {
hasLeftSidewalk = true;
wheelchairAttributesLeftSide.setSlopedKerbHeight(heights[0]);
}
if (heights[1] > -1) {
hasLeftSidewalk = true;
wheelchairAttributesRightSide.setSlopedKerbHeight(heights[1]);
}
}
/**
* Calculate the kerb height from the way that should be stored on the graph bsaed on the tag keys specified
*
* @param kerbTags The tag keys that should be evaluated
* @param initialValue The initial value for the return. If no kerb height info is found, this value is returned
* @return The value to use as the kerb height derived from the specified tag keys.
*/
private int calcSingleKerbHeightFromTagList(String[] kerbTags, int initialValue) {
int height = initialValue;
for (String kerbTag : kerbTags) {
int kerbHeightValue = convertKerbTagValueToCentimetres((String) cleanedTags.get(kerbTag));
if (kerbHeightValue != -1) {
height = kerbHeightValue;
}
}
return height;
}
/**
* Calculate the kerb heights from the way that should be stored on the graph bsaed on the tag keys specified.
* This method looks at the tags which specify a side to the road)
*
* @param kerbTags The tag keys that should be evaluated
* @param initialValues The initial value for the return. If no kerb height info is found, this value is returned
* @return The values to use as the kerb height derived from the specified tag keys. The first item
* in the array is for the left side, and the second is the right side.
*/
private int[] calcSingleKerbHeightFromSidedTagList(String[] kerbTags, int[] initialValues) {
int[] heights = initialValues;
int height = -1;
for (String kerbTag : kerbTags) {
String[] tagValues = getSidedKerbTagValuesToApply(kerbTag);
if (tagValues[0] != null && !tagValues[0].isEmpty()) {
height = convertKerbTagValueToCentimetres(tagValues[0].toLowerCase());
if (height > -1) {
heights[0] = height;
}
}
if (tagValues[1] != null && !tagValues[1].isEmpty()) {
height = convertKerbTagValueToCentimetres(tagValues[1].toLowerCase());
if (height > -1) {
heights[1] = height;
}
}
}
return heights;
}
/**
* Look at way and try to find the correct kerb heights for it. In some cases when the kerbs are attached directly to a way they are
* marked as start and end and so we need to look through the various tags to try and find these.
*
* @param key The base key that we are investigating (e.g. "kerb", "sloped_kerb" etc.)
* @return The textual tag that should be used as the kerb height
*/
private String[] getSidedKerbTagValuesToApply(String key) {
// If we are looking at the kerbs, sometimes the start and end of a way is marked as having different kerb
// heights using the ...:start and ...:end tags. For now, we just want to get the worse of these values (the
// highest)
double leftStart = -1;
double leftEnd = -1;
double rightStart = -1;
double rightEnd = -1;
String[] endValues = getSidedTagValue(key + ":end");
// Convert
if (endValues[0] != null && !endValues[0].isEmpty()) {
leftEnd = convertKerbTagValueToCentimetres(endValues[0]);
}
if (endValues[1] != null && !endValues[1].isEmpty()) {
rightEnd = convertKerbTagValueToCentimetres(endValues[1]);
}
String[] startValues = getSidedTagValue(key + ":start");
// Convert
if (startValues[0] != null && !startValues[0].isEmpty()) {
leftStart = convertKerbTagValueToCentimetres(startValues[0]);
}
if (startValues[1] != null && !startValues[1].isEmpty()) {
rightStart = convertKerbTagValueToCentimetres(startValues[1]);
}
// Now compare to find the worst
String[] values = new String[2];
if (leftEnd > leftStart)
values[0] = endValues[0];
else if (leftStart > leftEnd)
values[0] = startValues[0];
if (rightEnd > rightStart)
values[1] = endValues[1];
else if (rightStart > rightEnd)
values[1] = startValues[1];
return values;
}
/**
* Compare the attributes gained for the given property between the sidewalks on the left and the right hand side
* of the feature and identify which is worse. This is useful if for some reason the sidewalks can not be created
* as separate edges from the feature, in which case you would avoid the whole way if an attribute was seen as
* impassible.
*
* @param attr The attribute to be assessed (surface, smoothness etc.)
* @return The value that is seen as being the worst
*/
private int getWorseAttributeValueFromSeparateItems(WheelchairAttributes.Attribute attr) {
switch (attr) {
case SURFACE:
return Math.max(Math.max(wheelchairAttributesLeftSide.getSurfaceType(), wheelchairAttributesRightSide.getSurfaceType()),
wheelchairAttributes.getSurfaceType());
case SMOOTHNESS:
return Math.max(Math.max(wheelchairAttributesLeftSide.getSmoothnessType(), wheelchairAttributesRightSide.getSmoothnessType()),
wheelchairAttributes.getSmoothnessType());
case KERB:
return Math.max(Math.max(wheelchairAttributesLeftSide.getSlopedKerbHeight(), wheelchairAttributesRightSide.getSlopedKerbHeight()),
wheelchairAttributes.getSlopedKerbHeight());
case WIDTH:
// default value is 0, but this will always be returned so we need to do a check
int l = wheelchairAttributesLeftSide.getWidth();
int r = wheelchairAttributesRightSide.getWidth();
int w = wheelchairAttributes.getWidth();
if (l <= 0) l = Integer.MAX_VALUE;
if (r <= 0) r = Integer.MAX_VALUE;
if (w <= 0) w = Integer.MAX_VALUE;
int ret = Math.min(Math.min(l, r), w);
if (ret == Integer.MAX_VALUE) ret = 0;
return ret;
case TRACK:
return Math.max(Math.max(wheelchairAttributesLeftSide.getTrackType(), wheelchairAttributesRightSide.getTrackType()),
wheelchairAttributes.getTrackType());
case INCLINE:
return Math.max(Math.max(wheelchairAttributesLeftSide.getIncline(), wheelchairAttributesRightSide.getIncline()),
wheelchairAttributes.getIncline());
default:
return 0;
}
}
/**
* Process an individual edge which has been derived from the way and then store it in the storage.
*
* @param way The parent way feature
* @param edge The specific edge to be processed
*/
@Override
public void processEdge(ReaderWay way, EdgeIteratorState edge) {
// We want to copy so that we don't overwrite original values as this edge is only part of the way
WheelchairAttributes at = wheelchairAttributes.copy();
// Get the kerb heights for the individual edge as this may overwrite the original
int kerbHeight = getKerbHeightForEdge(way);
if (kerbHeight > -1) {
at.setSlopedKerbHeight(kerbHeight);
}
// Check for if we have specified which side the processing is for
if (way.hasTag("ors-sidewalk-side")) {
String side = way.getTag("ors-sidewalk-side");
if (side.equals(SW_VAL_LEFT)) {
// Only get the attributes for the left side
at = getAttributes(SW_VAL_LEFT);
at.setSide(WheelchairAttributes.Side.LEFT);
}
if (side.equals(SW_VAL_RIGHT)) {
at = getAttributes(SW_VAL_RIGHT);
at.setSide(WheelchairAttributes.Side.RIGHT);
}
} else {
// if we have sidewalks attached, then we should also look at those. We should only hit this point if
// the preprocessing hasn't detected that there are sidewalks even though there are...
if (hasRightSidewalk || hasLeftSidewalk) {
at = combineAttributesOfWayWhenBothSidesPresent(at);
}
}
storage.setEdgeValues(edge.getEdge(), at);
}
/**
* Get an overriding kerb height if needed from the nodes that are on the way rather than the data stored on the way itself.
* This should be the case if we are specifying to only store kerb heights on crossings as these features do not normally
* have kerb heights attached to them
*
* @param way The way that is being investigated
* @return A kerb height from the tags of the nodes on the way, or -1 if no kerb heights are found/required
*/
int getKerbHeightForEdge(ReaderWay way) {
int kerbHeight = -1;
if (!kerbHeightOnlyOnCrossing || (way.hasTag(KEY_FOOTWAY) && way.getTag(KEY_FOOTWAY).equals("crossing"))) {
// Look for kerb information
kerbHeight = getKerbHeightFromNodeTags();
}
return kerbHeight;
}
/**
* Look at the information stored against the nodes of the way and extract the kerb height to use for the whole way
* from those data.
*
* @return The derived kerb height in centimetres from teh nodes that are on the way
*/
int getKerbHeightFromNodeTags() {
// Assumed kerb heights are those obtained from a tag without the explicit :height attribute
List<Integer> assumedKerbHeights = new ArrayList<>();
// Explicit heights are those provided by the :height tag - these should take precidence
List<Integer> explicitKerbHeights = new ArrayList<>();
for (Map.Entry<Integer, Map<String, String>> entry : nodeTagsOnWay.entrySet()) {
Map<String, String> tags = entry.getValue();
for (Map.Entry<String, String> tag : tags.entrySet()) {
switch (tag.getKey()) {
case KEY_SLOPED_CURB, "curb", "kerb", KEY_SLOPED_KERB ->
assumedKerbHeights.add(convertKerbTagValueToCentimetres(tag.getValue()));
case KEY_KERB_HEIGHT -> explicitKerbHeights.add(convertKerbTagValueToCentimetres(tag.getValue()));
default -> {
}
}
}
}
if (!explicitKerbHeights.isEmpty()) {
return Collections.max(explicitKerbHeights);
} else if (!assumedKerbHeights.isEmpty()) {
// If we have multiple kerb heights, we need to apply the largest to the edge as this is the worst
return Collections.max(assumedKerbHeights);
} else {
return -1;
}
}
/**
* When sidewalks are tagged on both sides for a way and we do not want to process them as separate items then we need
* to get the "worst" for each attribute and use that.
*
* @param attributes The attributes storage object that needs to be merged
* @return A resultant combined object
*/
public WheelchairAttributes combineAttributesOfWayWhenBothSidesPresent(WheelchairAttributes attributes) {
WheelchairAttributes at = attributes;
int tr = getWorseAttributeValueFromSeparateItems(WheelchairAttributes.Attribute.TRACK);
if (tr > 0) at.setTrackType(tr);
int su = getWorseAttributeValueFromSeparateItems(WheelchairAttributes.Attribute.SURFACE);
if (su > 0) at.setSurfaceType(su);
int sm = getWorseAttributeValueFromSeparateItems(WheelchairAttributes.Attribute.SMOOTHNESS);
if (sm > 0) at.setSmoothnessType(sm);
int sl = getWorseAttributeValueFromSeparateItems(WheelchairAttributes.Attribute.KERB);
if (sl > 0) at.setSlopedKerbHeight(sl);
int wi = getWorseAttributeValueFromSeparateItems(WheelchairAttributes.Attribute.WIDTH);
if (wi > 0) at.setWidth(wi);
int in = getWorseAttributeValueFromSeparateItems(WheelchairAttributes.Attribute.INCLINE);
if (in > 0) at.setIncline(in);
at.setSurfaceQualityKnown(
wheelchairAttributesLeftSide.isSurfaceQualityKnown()
&& wheelchairAttributesRightSide.isSurfaceQualityKnown()
&& attributes.isSurfaceQualityKnown()
);
at.setSuitable(
wheelchairAttributesLeftSide.isSuitable()
&& wheelchairAttributesRightSide.isSuitable()
&& attributes.isSuitable()
);
return at;
}
/**
* Get the attributes of a sidewalk on the specified side of the road
*
* @param side The side you want the attributes for
* @return A WheelchairAttributes object for the side requested. If there are no attributes for the specified
* side, then the overall attributes for the way are returned
*/
private WheelchairAttributes getAttributes(String side) {
WheelchairAttributes at = wheelchairAttributes.copy();
// Now get the specific items
switch (side) {
case SW_VAL_LEFT -> at = at.merge(wheelchairAttributesLeftSide);
case SW_VAL_RIGHT -> at = at.merge(wheelchairAttributesRightSide);
default -> {
}
}
return at;
}
/**
* Converts a kerb height value to a numerical height (in centimetres). A kerb could be stored as an explicit height or
* as an indicator as to whether the kerb is lowered or not.
*
* @param value The value of the tag
* @return The presumed height of the kerb in metres
*/
private int convertKerbTagValueToCentimetres(String value) {
int centimetreHeight = -1;
if (value == null) {
return -1;
}
switch (value) {
case "yes", KEY_BOTH, "low", "lowered", "dropped", "sloped" -> centimetreHeight = 3;
case "no", "none", "one", "rolled", "regular" -> centimetreHeight = 15;
case "at_grade", "flush" -> centimetreHeight = 0;
default -> {
double metresHeight = UnitsConverter.convertOSMDistanceTagToMeters(value);
// If no unit was given in the tag, the value might be in meters or centimeters; we can only guess
// depending on the value
if (metresHeight < 0.15) {
centimetreHeight = (int) (metresHeight * 100);
} else {
centimetreHeight = (int) metresHeight;
}
}
}
return centimetreHeight;
}
/**
* Get the values obtained from a way for a specific sidewalk property. For example, providing the property
* "surface" would check the way for the surface tag stored against attached sidewalks using the keys
* sidewalk:left:surface, sidewalk:right:surface, and sidewalk:both:surface. The obtained values are then returned
* in an array.
*
* @param property The property to be extracted
* @return A String array containing two values - the first is the property for the left sidewalk and
* the second is the property value for the right sidewalk.
*/
private String[] getSidedTagValue(String property) {
String[] values = new String[2];
// Left side
if (cleanedTags.containsKey("sidewalk:left:" + property))
values[0] = (String) cleanedTags.get("sidewalk:left:" + property);
else if (cleanedTags.containsKey("footway:left:" + property))
values[0] = (String) cleanedTags.get("footway:left:" + property);
// Right side
if (cleanedTags.containsKey("sidewalk:right:" + property))
values[1] = (String) cleanedTags.get("sidewalk:right:" + property);
else if (cleanedTags.containsKey("footway:right:" + property))
values[1] = (String) cleanedTags.get("footway:right:" + property);
// Both
if (cleanedTags.containsKey(KEY_SIDEWALK_BOTH + property)) {
values[0] = (String) cleanedTags.get(KEY_SIDEWALK_BOTH + property);
values[1] = (String) cleanedTags.get(KEY_SIDEWALK_BOTH + property);
} else if (cleanedTags.containsKey(KEY_FOOTWAY_BOTH + property)) {
values[0] = (String) cleanedTags.get(KEY_FOOTWAY_BOTH + property);
values[1] = (String) cleanedTags.get(KEY_FOOTWAY_BOTH + property);
}
return values;
}
private int getInclineFromTagValue(String inclineValue) {
double decimalIncline = UnitsConverter.convertOSMInclineValueToPercentage(inclineValue, true);
decimalIncline = Math.min(decimalIncline, 15.0);
return (int) Math.round(decimalIncline);
}
/**
* Determine if the way is a separate footway object or a road feature.
*
* @param way The OSM way object to be assessed
* @return Whether the way is seen as a separately drawn footway (true) or a road (false)
*/
private boolean isSeparateFootway(ReaderWay way) {
String type = way.getTag("highway", "");
String[] pedestrianWayTypes = {
"living_street",
"pedestrian",
KEY_FOOTWAY,
"path",
"crossing",
"track"
};
// Check if it is a footpath or pedestrian
if (!type.isEmpty()) {
// We are looking at a separate footpath
// we are looking at a road feature so any footway would be attached to it as a tag
return Arrays.asList(pedestrianWayTypes).contains(type);
}
return true;
}
@Override
public String getName() {
return "Wheelchair";
}
}