| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.Linq; |
| | 4 | | using System.Runtime.CompilerServices; |
| | 5 | | using System.Text.Json; |
| | 6 | | using System.Text.RegularExpressions; |
| | 7 | | using Itinero.Instructions.ToText; |
| | 8 | | using Itinero.Instructions.Types; |
| | 9 | | using Itinero.Instructions.Types.Generators; |
| | 10 | |
|
| | 11 | | [assembly: InternalsVisibleTo("Itinero.Tests.Instructions")] |
| | 12 | |
|
| | 13 | | namespace Itinero.Instructions.Configuration; |
| | 14 | |
|
| | 15 | | internal static class ConfigurationParser |
| | 16 | | { |
| 1 | 17 | | private static readonly Regex RenderValueRegex = |
| 1 | 18 | | new(@"^(\${[.+-]?[a-zA-Z0-9_]+}|\$[.+-]?[a-zA-Z0-9_]+|[^\$]+)*$"); |
| | 19 | |
|
| 1 | 20 | | private static readonly List<(string, Predicate<(string? a, string? b)>)> Operators = |
| 1 | 21 | | new() |
| 1 | 22 | | { |
| 1 | 23 | | // This is a list, as we first need to match '<=' and '>=', otherwise we might think the match is "abc<" = " |
| 44 | 24 | | ("<=", t => BothDouble(t, d => d.a <= d.b)), |
| 24 | 25 | | (">=", t => BothDouble(t, d => d.a >= d.b)), |
| 0 | 26 | | ("!=", t => t.a != null && t.b != null && t.a != t.b), |
| 2 | 27 | | ("=", t => t.a != null && t.b != null && t.a == t.b), |
| 22 | 28 | | ("<", t => BothDouble(t, d => d.a < d.b)), |
| 46 | 29 | | (">", t => BothDouble(t, d => d.a > d.b)) |
| 1 | 30 | | }; |
| | 31 | |
|
| | 32 | | /// <summary> |
| | 33 | | /// Parses the full pipeline |
| | 34 | | /// </summary> |
| | 35 | | /// <param name="jsonElement">The json element to start from.</param> |
| | 36 | | /// <param name="knownGenerators"> |
| | 37 | | /// The instruction generators that can be used during the construction - should include them |
| | 38 | | /// all explicitely |
| | 39 | | /// </param> |
| | 40 | | /// <returns>The instruction generator and the to text translators.</returns> |
| | 41 | | public static (IReadOnlyList<IInstructionGenerator> generator, Dictionary<string, IInstructionToText> toTexts) |
| | 42 | | ParseRouteToInstructions(JsonElement jsonElement, Dictionary<string, IInstructionGenerator> knownGenerators) |
| 2 | 43 | | { |
| | 44 | | // parse generator names and instantiate generators. |
| 2 | 45 | | var generatorNames = jsonElement.GetProperty("generators").EnumerateArray().Select(v => v.GetString()) |
| 2 | 46 | | .ToList(); |
| 2 | 47 | | var generators = generatorNames.Select(name => |
| 0 | 48 | | { |
| 0 | 49 | | if (knownGenerators.TryGetValue(name, out var g)) |
| 0 | 50 | | { |
| 0 | 51 | | return g; |
| 2 | 52 | | } |
| 2 | 53 | |
|
| 0 | 54 | | throw new Exception($"Generator not found: {name}"); |
| 2 | 55 | | }).ToList(); |
| | 56 | |
|
| | 57 | | // parse instructions to text configurations. |
| 2 | 58 | | var languages = jsonElement.GetProperty("languages"); |
| 2 | 59 | | var toTexts = new Dictionary<string, IInstructionToText>(); |
| 10 | 60 | | foreach (var obj in languages.EnumerateObject()) |
| 2 | 61 | | { |
| 2 | 62 | | var langCode = obj.Name; |
| 2 | 63 | | var wholeToText = new Box<IInstructionToText>(); |
| 2 | 64 | | var whole = ParseInstructionToText(obj.Value, wholeToText, |
| 2 | 65 | | new Dictionary<string, IInstructionToText>(), "/"); |
| 2 | 66 | | wholeToText.Content = whole; |
| 2 | 67 | | toTexts[langCode] = whole; |
| 2 | 68 | | } |
| | 69 | |
|
| 2 | 70 | | return (generators, toTexts); |
| 2 | 71 | | } |
| | 72 | |
|
| | 73 | | /** |
| | 74 | | * Parses a JSON-file and converts it into a InstructionToText. |
| | 75 | | * This is done as following: |
| | 76 | | * |
| | 77 | | * A hash is interpreted as being "condition":"rendervalue" |
| | 78 | | * |
| | 79 | | * A condition is parsed in the following way: |
| | 80 | | * |
| | 81 | | * "InstructionType": the 'type' of the instruction must match the given string, e.g. 'Roundabout', 'Start', 'Base', |
| | 82 | | * These are the same as the classnames of 'Instruction/*.cs' (but not case sensitive and the Instruction can be dro |
| | 83 | | * |
| | 84 | | * "extensions": is a special key. The containing object's top levels have keys which are calculated and 'injected' |
| | 85 | | * |
| | 86 | | * "$someVariable": some variable has to exist. |
| | 87 | | * |
| | 88 | | * "condition1&condition2": all the conditions have to match |
| | 89 | | * |
| | 90 | | * If the condition contains a "="," |
| | 91 | | * < |
| | 92 | | * "," |
| | 93 | | * > |
| | 94 | | * "," |
| | 95 | | * < |
| | 96 | | * = |
| | 97 | | * " or " |
| | 98 | | * > |
| | 99 | | * =" then both parts are interpreted as renderValues and should satisfy the condition. |
| | 100 | | * (If a substitution fails in any part, the condition is considered failed) |
| | 101 | | * (Note that = compares the string values, whereas the comparators first parse to a double. If parsing fails, the c |
| | 102 | | * |
| | 103 | | * If the condition equals "*", then this is considered the 'fallback'-value. This implies that if every other condi |
| | 104 | | * |
| | 105 | | * |
| | 106 | | * A rendervalue is a string such as "Take the {exitNumber}th exit", where 'exitNumber' is substituted by the corres |
| | 107 | | * If that substitution fails, the result will be null which will either cause an error in rendering or a condition |
| | 108 | | * |
| | 109 | | * Other notes: |
| | 110 | | * A POSITIVE angle is going left, |
| | 111 | | * A NEGATIVE angle is going right |
| | 112 | | */ |
| | 113 | | internal static IInstructionToText ParseInstructionToText(JsonElement jobj, |
| | 114 | | Box<IInstructionToText>? wholeToText = null, |
| | 115 | | Dictionary<string, IInstructionToText>? extensions = null, string context = "") |
| 17 | 116 | | { |
| 17 | 117 | | extensions ??= new Dictionary<string, IInstructionToText>(); |
| 17 | 118 | | var conditions = new List<(Predicate<BaseInstruction>, IInstructionToText)>(); |
| 17 | 119 | | var lowPriority = new List<(Predicate<BaseInstruction>, IInstructionToText)>(); |
| | 120 | |
|
| 157 | 121 | | foreach (var obj in jobj.EnumerateObject()) |
| 53 | 122 | | { |
| 53 | 123 | | var key = obj.Name; |
| 53 | 124 | | var value = obj.Value; |
| | 125 | |
|
| 53 | 126 | | if (key == "extensions") |
| 2 | 127 | | { |
| 2 | 128 | | var extensionsSource = obj.Value; |
| 10 | 129 | | foreach (var ext in extensionsSource.EnumerateObject()) |
| 2 | 130 | | { |
| 2 | 131 | | extensions.Add(ext.Name, |
| 2 | 132 | | ParseSubObj(ext.Value, context + ".extensions." + ext.Name, extensions, wholeToText) |
| 2 | 133 | | ); |
| 2 | 134 | | } |
| | 135 | |
|
| 2 | 136 | | continue; |
| | 137 | | } |
| | 138 | |
|
| 51 | 139 | | var (p, isLowPriority) = ParseCondition(key, wholeToText, context + "." + key, extensions); |
| 51 | 140 | | var sub = ParseSubObj(value, context + "." + key, extensions, wholeToText); |
| 51 | 141 | | (isLowPriority ? lowPriority : conditions).Add((p, sub)); |
| 51 | 142 | | } |
| | 143 | |
|
| 17 | 144 | | return new ConditionalToText(conditions.Concat(lowPriority).ToList(), context); |
| 17 | 145 | | } |
| | 146 | |
|
| | 147 | | private static IInstructionToText ParseSubObj(JsonElement j, string context, |
| | 148 | | Dictionary<string, IInstructionToText> extensions, Box<IInstructionToText>? wholeToText) |
| 53 | 149 | | { |
| 53 | 150 | | if (j.ValueKind == JsonValueKind.String) |
| 45 | 151 | | { |
| 45 | 152 | | return ParseRenderValue(j.GetString(), extensions, wholeToText, context); |
| | 153 | | } |
| | 154 | |
|
| 8 | 155 | | return ParseInstructionToText(j, wholeToText, extensions, context); |
| 53 | 156 | | } |
| | 157 | |
|
| | 158 | | private static bool BothDouble((string? a, string? b) t, Predicate<(double a, double b)> p) |
| 68 | 159 | | { |
| 68 | 160 | | if (double.TryParse(t.a, out var a) && double.TryParse(t.b, out var b)) |
| 68 | 161 | | { |
| 68 | 162 | | return p.Invoke((a, b)); |
| | 163 | | } |
| | 164 | |
|
| 0 | 165 | | return false; |
| 68 | 166 | | } |
| | 167 | |
|
| | 168 | | internal static (Predicate<BaseInstruction> predicate, bool lowPriority) ParseCondition(string condition, |
| | 169 | | Box<IInstructionToText>? wholeToText = null, |
| | 170 | | string context = "", |
| | 171 | | Dictionary<string, IInstructionToText>? extensions = null) |
| 98 | 172 | | { |
| 98 | 173 | | if (condition == "*") |
| 12 | 174 | | { |
| 25 | 175 | | return (_ => true, true); |
| | 176 | | } |
| | 177 | |
|
| 86 | 178 | | if (condition.IndexOf("&", StringComparison.Ordinal) >= 0) |
| 18 | 179 | | { |
| 18 | 180 | | var cs = condition.Split("&") |
| 40 | 181 | | .Select((condition1, i) => ParseCondition(condition1, wholeToText, context + "&" + i, extensions)) |
| 58 | 182 | | .Select(t => t.predicate); |
| 78 | 183 | | return (instruction => cs.All(p => p.Invoke(instruction)), false); |
| | 184 | | } |
| | 185 | |
|
| 696 | 186 | | foreach (var (key, op) in Operators) |
| 273 | 187 | | { |
| | 188 | | // We iterate over all the possible operator keywords: '=', '<=', '>=', ... |
| | 189 | | // If they are found, we apply the actual operator |
| 273 | 190 | | if (condition.IndexOf(key, StringComparison.Ordinal) < 0) |
| 219 | 191 | | { |
| 219 | 192 | | continue; |
| | 193 | | } |
| | 194 | |
|
| | 195 | | // Get the two parts of the condition... |
| 54 | 196 | | var parts = condition.Split(key).Select(renderValue => |
| 108 | 197 | | ParseRenderValue(renderValue, extensions, wholeToText, context + "." + key, false)) |
| 54 | 198 | | .ToList(); |
| 54 | 199 | | if (parts.Count != 2) |
| 0 | 200 | | { |
| 0 | 201 | | throw new ArgumentException("Parsing condition " + condition + |
| 0 | 202 | | " failed, it has an operator, but to much matches. Maybe you forgot to add a |
| | 203 | | } |
| | 204 | |
|
| | 205 | | // And apply the instruction on it! |
| | 206 | | // We pull the instruction from thin air by returning a lambda instead |
| 54 | 207 | | return ( |
| 70 | 208 | | instruction => op.Invoke((parts[0].ToText(instruction), parts[1].ToText(instruction))), |
| 54 | 209 | | false); |
| | 210 | | } |
| | 211 | |
|
| | 212 | | // At this point, the condition is a single string |
| | 213 | | // This could either be a type matching or a substitution that has to exist |
| 14 | 214 | | var rendered = ParseRenderValue(condition, extensions, wholeToText, context, false); |
| 14 | 215 | | if (rendered.SubstitutedValueCount() > 0) |
| 3 | 216 | | { |
| 8 | 217 | | return (instruction => rendered.ToText(instruction) != null, false); |
| | 218 | | } |
| | 219 | |
|
| 11 | 220 | | return ( |
| 27 | 221 | | instruction => string.Equals(instruction.Type, condition, StringComparison.CurrentCultureIgnoreCase), |
| 11 | 222 | | false); |
| 98 | 223 | | } |
| | 224 | |
|
| | 225 | | internal static SubstituteText ParseRenderValue(string value, |
| | 226 | | Dictionary<string, IInstructionToText>? extensions = null, |
| | 227 | | Box<IInstructionToText>? wholeToText = null, |
| | 228 | | string context = "", |
| | 229 | | bool crashOnNotFound = true) |
| 169 | 230 | | { |
| 169 | 231 | | var parts = RenderValueRegex.Match(value).Groups[1].Captures |
| 169 | 232 | | .Select(m => |
| 211 | 233 | | { |
| 211 | 234 | | var v = m.Value; |
| 211 | 235 | | if (!v.StartsWith("$")) |
| 119 | 236 | | { |
| 119 | 237 | | return (m.Value, false); |
| 169 | 238 | | } |
| 169 | 239 | |
|
| 92 | 240 | | v = v.Substring(1).Trim('{', '}').ToLower(); |
| 92 | 241 | | return (v, true); |
| 211 | 242 | | }).ToList() |
| 169 | 243 | | ; |
| 169 | 244 | | return new SubstituteText(parts, wholeToText, context, extensions, crashOnNotFound); |
| 169 | 245 | | } |
| | 246 | | } |