| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.Linq; |
| | 4 | | using OsmSharp; |
| | 5 | | using OsmSharp.Db; |
| | 6 | |
|
| | 7 | | namespace Itinero.IO.Osm.Restrictions; |
| | 8 | |
|
| | 9 | | /// <summary> |
| | 10 | | /// Parses OSM restrictions. Input is an OSM relation restriction, output is one or more restricted sequences of vertice |
| | 11 | | /// </summary> |
| | 12 | | /// <remarks> |
| | 13 | | /// This was used a the main source: https://wiki.openstreetmap.org/wiki/Relation:restriction |
| | 14 | | /// </remarks> |
| | 15 | | public class OsmTurnRestrictionParser |
| | 16 | | { |
| | 17 | | /// <summary> |
| | 18 | | /// The default restriction types and their prohibitiveness. |
| | 19 | | /// </summary> |
| 1 | 20 | | public static readonly IReadOnlyDictionary<string, bool> DefaultRestrictionTypes = new Dictionary<string, bool> |
| 1 | 21 | | { |
| 1 | 22 | | { "no_left_turn", true }, |
| 1 | 23 | | { "no_right_turn", true }, |
| 1 | 24 | | { "no_straight_on", true }, |
| 1 | 25 | | { "no_u_turn", true }, |
| 1 | 26 | | { "no_entry", true }, |
| 1 | 27 | | { "no_exit", true }, |
| 1 | 28 | | { "only_right_turn", false }, |
| 1 | 29 | | { "only_left_turn", false }, |
| 1 | 30 | | { "only_straight_on", false }, |
| 1 | 31 | | { "only_u_turn", false } |
| 1 | 32 | | }; |
| | 33 | |
|
| | 34 | | /// <summary> |
| | 35 | | /// The default vehicle types list. |
| | 36 | | /// </summary> |
| 1 | 37 | | public static readonly ISet<string> DefaultVehicleTypes = new HashSet<string>() |
| 1 | 38 | | { |
| 1 | 39 | | "motor_vehicle", |
| 1 | 40 | | "foot", |
| 1 | 41 | | "dog", |
| 1 | 42 | | "bicycle", |
| 1 | 43 | | "motorcar", |
| 1 | 44 | | "hgv", |
| 1 | 45 | | "psv", |
| 1 | 46 | | "emergency" |
| 1 | 47 | | }; |
| | 48 | |
|
| | 49 | | /// <summary> |
| | 50 | | /// The default vehicle type if none is specified. |
| | 51 | | /// </summary> |
| 1 | 52 | | public static string DefaultVehicleType = "motor_vehicle"; |
| | 53 | |
|
| | 54 | | private readonly ISet<string> _vehicleTypes; |
| | 55 | | private readonly IReadOnlyDictionary<string, bool> _restrictionTypes; |
| | 56 | | private readonly string _defaultVehicleType; |
| | 57 | |
|
| | 58 | | /// <summary> |
| | 59 | | /// Creates a new restriction parser. |
| | 60 | | /// </summary> |
| | 61 | | /// <param name="vehicleTypes">The vehicle types.</param> |
| | 62 | | /// <param name="restrictionTypes">The restriction types.</param> |
| | 63 | | /// <param name="defaultVehicleType">The default vehicle type.</param> |
| 8 | 64 | | public OsmTurnRestrictionParser(ISet<string>? vehicleTypes = null, |
| 8 | 65 | | IReadOnlyDictionary<string, bool>? restrictionTypes = null, |
| 8 | 66 | | string? defaultVehicleType = null) |
| 8 | 67 | | { |
| 8 | 68 | | _defaultVehicleType = defaultVehicleType ?? DefaultVehicleType; |
| 8 | 69 | | _vehicleTypes = vehicleTypes ?? DefaultVehicleTypes; |
| 8 | 70 | | _restrictionTypes = restrictionTypes ?? DefaultRestrictionTypes; |
| 8 | 71 | | } |
| | 72 | |
|
| | 73 | | /// <summary> |
| | 74 | | /// Trims the attributes collection keeping only attributes that are relevant to represent a restriction. |
| | 75 | | /// </summary> |
| | 76 | | /// <param name="attributes">The attributes.</param> |
| | 77 | | /// <returns>The relevant attributes.</returns> |
| | 78 | | public IEnumerable<(string key, string value)> Trim(IEnumerable<(string key, string value)> attributes) |
| 0 | 79 | | { |
| 0 | 80 | | foreach (var (key, value) in attributes) |
| 0 | 81 | | { |
| 0 | 82 | | if (key.StartsWith("restriction:")) |
| 0 | 83 | | { |
| 0 | 84 | | var vehicleType = key[12..]; |
| 0 | 85 | | if (!_vehicleTypes.Contains(vehicleType)) continue; |
| 0 | 86 | | if (!_restrictionTypes.ContainsKey(value)) continue; |
| 0 | 87 | | } |
| | 88 | | else |
| 0 | 89 | | { |
| 0 | 90 | | switch (key) |
| | 91 | | { |
| | 92 | | case "type": |
| 0 | 93 | | if (value != "restriction") continue; |
| 0 | 94 | | break; |
| | 95 | | case "restriction": |
| 0 | 96 | | if (!_restrictionTypes.ContainsKey(value)) continue; |
| 0 | 97 | | break; |
| | 98 | | case "except": |
| 0 | 99 | | var exceptions = value.Split(';').ToList(); |
| 0 | 100 | | exceptions.RemoveAll(v => !_vehicleTypes.Contains(v)); |
| 0 | 101 | | if (exceptions.Count == 0) continue; |
| 0 | 102 | | break; |
| | 103 | | } |
| 0 | 104 | | } |
| | 105 | |
|
| 0 | 106 | | yield return (key, value); |
| 0 | 107 | | } |
| 0 | 108 | | } |
| | 109 | |
|
| | 110 | | /// <summary> |
| | 111 | | /// Returns true if by the tags only, the relation is a turn restriction. |
| | 112 | | /// </summary> |
| | 113 | | /// <param name="relation">The relation.</param> |
| | 114 | | /// <param name="isProhibitive">True if the restriction restricts, false if it allows the turn it represents.</param |
| | 115 | | /// <returns>True if the relation is a restriction.</returns> |
| | 116 | | public bool IsRestriction(Relation relation, out bool isProhibitive) |
| 4 | 117 | | { |
| 4 | 118 | | isProhibitive = false; |
| 4 | 119 | | var vehicleType = string.Empty; |
| 4 | 120 | | var exceptions = new List<string>(); |
| | 121 | |
|
| 4 | 122 | | if (relation?.Tags == null) |
| 0 | 123 | | { |
| 0 | 124 | | return false; |
| | 125 | | } |
| | 126 | |
|
| 4 | 127 | | if (!relation.Tags.Contains("type", "restriction")) |
| 0 | 128 | | { |
| 0 | 129 | | return false; |
| | 130 | | } |
| | 131 | |
|
| 4 | 132 | | if (relation.Tags.TryGetValue("except", out var exceptValue)) |
| 0 | 133 | | { |
| 0 | 134 | | exceptions.AddRange(exceptValue.Split(';')); |
| 0 | 135 | | } |
| | 136 | |
|
| 4 | 137 | | if (!relation.Tags.TryGetValue("restriction", out var restrictionValue)) |
| 0 | 138 | | { |
| 0 | 139 | | foreach (var tag in relation.Tags) |
| 0 | 140 | | { |
| 0 | 141 | | if (!tag.Key.StartsWith("restriction:")) continue; |
| | 142 | |
|
| 0 | 143 | | vehicleType = tag.Key[12..]; |
| 0 | 144 | | restrictionValue = tag.Value; |
| 0 | 145 | | break; |
| | 146 | | } |
| 0 | 147 | | } |
| | 148 | | else |
| 4 | 149 | | { |
| 4 | 150 | | vehicleType = _defaultVehicleType; |
| 4 | 151 | | } |
| | 152 | |
|
| 4 | 153 | | if (restrictionValue == null) |
| 0 | 154 | | { |
| 0 | 155 | | return false; |
| | 156 | | } |
| | 157 | |
|
| 4 | 158 | | if (string.IsNullOrWhiteSpace(vehicleType)) |
| 0 | 159 | | { |
| 0 | 160 | | return false; |
| | 161 | | } |
| | 162 | |
|
| 4 | 163 | | if (!_vehicleTypes.Contains(vehicleType)) |
| 0 | 164 | | { |
| 0 | 165 | | return false; |
| | 166 | | } |
| | 167 | |
|
| 4 | 168 | | exceptions.RemoveAll(v => !_vehicleTypes.Contains(v)); |
| | 169 | |
|
| 4 | 170 | | if (!_restrictionTypes.TryGetValue(restrictionValue, out isProhibitive)) |
| 0 | 171 | | { |
| 0 | 172 | | return false; |
| | 173 | | } |
| | 174 | |
|
| 4 | 175 | | return true; |
| 4 | 176 | | } |
| | 177 | |
|
| | 178 | | /// <summary> |
| | 179 | | /// Tries to parse an OSM Relation into an OSM turn restriction. |
| | 180 | | /// </summary> |
| | 181 | | /// <param name="relation">The restriction relation.</param> |
| | 182 | | /// <param name="getMemberWay">Function to get the member way.</param> |
| | 183 | | /// <param name="restriction">The turn restriction.</param> |
| | 184 | | /// <returns>Returns true if the relation is a restriction, false if not. Returns an error result if parsing failed |
| | 185 | | public Result<bool> TryParse(Relation relation, Func<long, Way?> getMemberWay, |
| | 186 | | out OsmTurnRestriction? restriction) |
| 3 | 187 | | { |
| 3 | 188 | | restriction = null; |
| 3 | 189 | | if (!this.IsRestriction(relation, out var isProhibitive)) return false; |
| | 190 | |
|
| 3 | 191 | | var froms = new List<Way>(); |
| 3 | 192 | | var vias = new List<Way>(); |
| 3 | 193 | | var tos = new List<Way>(); |
| 3 | 194 | | long? viaNodeId = null; |
| 27 | 195 | | foreach (var m in relation.Members ?? ArraySegment<RelationMember>.Empty) |
| 9 | 196 | | { |
| 9 | 197 | | if (m == null) return new Result<bool>("not all members set"); |
| 9 | 198 | | if (m.Role != "via" && m.Role != "from" && m.Role != "to") continue; |
| | 199 | |
|
| 9 | 200 | | switch (m.Type) |
| | 201 | | { |
| | 202 | | case OsmGeoType.Node: |
| 2 | 203 | | if (m.Role == "via") |
| 2 | 204 | | { |
| 2 | 205 | | viaNodeId = m.Id; |
| 2 | 206 | | } |
| 2 | 207 | | break; |
| | 208 | | case OsmGeoType.Way: |
| 7 | 209 | | if (m.Role is not "from" and not "to" and not "via") continue; |
| 7 | 210 | | var wayMember = getMemberWay(m.Id); |
| 7 | 211 | | if (wayMember == null) return new Result<bool>("member way not found"); |
| 7 | 212 | | switch (m.Role) |
| | 213 | | { |
| | 214 | | case "via": |
| 1 | 215 | | vias.Add(wayMember); |
| 1 | 216 | | break; |
| | 217 | | case "from": |
| 3 | 218 | | froms.Add(wayMember); |
| 3 | 219 | | break; |
| | 220 | | default: |
| 3 | 221 | | tos.Add(wayMember); |
| 3 | 222 | | break; |
| | 223 | | } |
| 7 | 224 | | break; |
| | 225 | | case OsmGeoType.Relation: |
| 0 | 226 | | break; |
| | 227 | | default: |
| 0 | 228 | | break; |
| | 229 | | } |
| 9 | 230 | | } |
| | 231 | |
|
| | 232 | | // 2 options: |
| | 233 | | // - with via-node. |
| | 234 | | // - with via-way(s). |
| | 235 | |
|
| 9 | 236 | | var attributes = relation.Tags?.Select(tag => (tag.Key, tag.Value)).ToArray() ?? |
| 3 | 237 | | ArraySegment<(string key, string value)>.Empty; |
| 3 | 238 | | if (viaNodeId != null) |
| 2 | 239 | | { |
| 2 | 240 | | restriction = OsmTurnRestriction.Create(froms, viaNodeId.Value, tos, isProhibitive, attributes); |
| 2 | 241 | | } |
| 1 | 242 | | else if (vias.Count > 0) |
| 1 | 243 | | { |
| 1 | 244 | | restriction = OsmTurnRestriction.Create(froms, vias, tos, isProhibitive, attributes); |
| 1 | 245 | | } |
| | 246 | | else |
| 0 | 247 | | { |
| 0 | 248 | | return new Result<bool>("no via nodes found"); |
| | 249 | | } |
| | 250 | |
|
| 3 | 251 | | return true; |
| 3 | 252 | | } |
| | 253 | | } |