Add documentation output

This commit is contained in:
Pieter Vander Vennet 2022-05-04 15:07:57 +02:00
parent df63111009
commit b877fff2b5
19 changed files with 643 additions and 441 deletions

View file

@ -7,14 +7,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0"/>
<PackageReference Include="xunit" Version="2.4.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0"/>
<PackageReference Include="coverlet.collector" Version="1.0.1"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AspectedRouting\AspectedRouting.csproj" />
<ProjectReference Include="..\AspectedRouting\AspectedRouting.csproj"/>
</ItemGroup>
</Project>

View file

@ -11,6 +11,21 @@ namespace AspectedRouting.Test
{
public class FunctionsTest
{
private readonly string constString = "{\"$const\": \"a\"}";
private readonly string IfDottedConditionJson
= "{" +
"\"$ifdotted\": {\"$eq\": \"yes\"}," +
"\"then\":{\"$const\": \"a\"}," +
"\"else\": {\"$const\": \"b\"}" +
"}";
private readonly string IfSimpleConditionJson
= "{" +
"\"$if\": true," +
"\"then\":\"thenResult\"," +
"\"else\": \"elseResult\"}";
private IExpression MustMatchJson()
{
var json = "{" +
@ -32,10 +47,9 @@ namespace AspectedRouting.Test
[Fact]
public void TestAll_AllTags_Yes()
{
var tagsAx = new Dictionary<string, string>
{
{"a", "b"},
{"x", "y"}
var tagsAx = new Dictionary<string, string> {
{ "a", "b" },
{ "x", "y" }
};
var expr = new Apply(MustMatchJson(), new Constant(tagsAx)).Optimize();
@ -46,9 +60,8 @@ namespace AspectedRouting.Test
[Fact]
public void TestAll_NoMatch_No()
{
var tagsAx = new Dictionary<string, string>
{
{"a", "b"},
var tagsAx = new Dictionary<string, string> {
{ "a", "b" }
};
var expr = new Apply(MustMatchJson(), new Constant(tagsAx)).Optimize();
@ -59,10 +72,9 @@ namespace AspectedRouting.Test
[Fact]
public void TestAll_NoMatchDifferent_No()
{
var tagsAx = new Dictionary<string, string>
{
{"a", "b"},
{"x", "someRandomValue"}
var tagsAx = new Dictionary<string, string> {
{ "a", "b" },
{ "x", "someRandomValue" }
};
var expr = new Apply(MustMatchJson(), new Constant(tagsAx)).Optimize();
@ -70,19 +82,6 @@ namespace AspectedRouting.Test
Assert.Equal("no", result);
}
private string IfDottedConditionJson
= "{" +
"\"$ifdotted\": {\"$eq\": \"yes\"}," +
"\"then\":{\"$const\": \"a\"}," +
"\"else\": {\"$const\": \"b\"}" +
"}";
private string IfSimpleConditionJson
= "{" +
"\"$if\": true," +
"\"then\":\"thenResult\"," +
"\"else\": \"elseResult\"}";
[Fact]
public void TestParsing_SimpleIf_CorrectExpression()
{
@ -129,9 +128,6 @@ namespace AspectedRouting.Test
Assert.Equal("b", resultF);
}
private string constString = "{\"$const\": \"a\"}";
[Fact]
public void Parse_ConstString_TypeIsFree()
{
@ -231,8 +227,7 @@ namespace AspectedRouting.Test
var e = new Var("e");
var f = new Var("f");
var newTypes = Funcs.Const.Types.RenameVars(new[]
{
var newTypes = Funcs.Const.Types.RenameVars(new[] {
new Curry(e, e),
new Curry(new Curry(b, f), new Curry(new Curry(a, b), new Curry(a, f)))
}).ToList();
@ -308,9 +303,9 @@ namespace AspectedRouting.Test
var unifB = tags2pdouble.Unify(tags2double, true);
Assert.NotNull(unifB);
var unifC = tags2double.Unify(tags2pdouble, false);
var unifC = tags2double.Unify(tags2pdouble);
Assert.NotNull(unifC);
var unifD = tags2pdouble.Unify(tags2double, false);
var unifD = tags2pdouble.Unify(tags2double);
Assert.Null(unifD);
}
@ -323,7 +318,7 @@ namespace AspectedRouting.Test
Typs.String,
new Curry(Typs.String, Typs.Bool));
var f0 = f.Specialize(strstrb);
Assert.Equal(new[] {strstrb}, f0.Types);
Assert.Equal(new[] { strstrb }, f0.Types);
var strstrstr = new Curry(
Typs.String,
@ -331,7 +326,7 @@ namespace AspectedRouting.Test
var f1 = f.Specialize(strstrstr);
Assert.Equal(new[] {strstrb, strstrstr}, f1.Types);
Assert.Equal(new[] { strstrb, strstrstr }, f1.Types);
}
[Fact]
@ -341,11 +336,11 @@ namespace AspectedRouting.Test
var p1 = Funcs.Const.Apply(new Constant(1.0)).Specialize(
new Curry(new Var("a"), Typs.Double));
var exprs = new[] {p0, p1};
var exprs = new[] { p0, p1 };
var newTypes = exprs.SpecializeToCommonTypes(out var _);
Assert.Single(newTypes);
exprs = new[] {p1, p0};
exprs = new[] { p1, p0 };
newTypes = exprs.SpecializeToCommonTypes(out var _);
Assert.Single(newTypes);
}
@ -360,7 +355,7 @@ namespace AspectedRouting.Test
Assert.Null(result);
}
[Fact]
public void ParseFunction_Duration_TotalMinutes()
{
@ -376,10 +371,8 @@ namespace AspectedRouting.Test
{
var e = new Apply(new Apply(Funcs.Default, new Constant("a")), Funcs.Id);
Assert.Single(e.Types);
Assert.Equal("string -> string", e.Types.First().ToString());
}
}
}

View file

@ -1,5 +1,4 @@
using System.Collections.Generic;
using AspectedRouting.IO.itinero1;
using AspectedRouting.IO.LuaSkeleton;
using AspectedRouting.Language;
using AspectedRouting.Language.Functions;
@ -13,12 +12,11 @@ namespace AspectedRouting.Test
public void ToLua_SimpleMapping_Table()
{
var mapping = new Mapping(
new[] {"a", "b", "c"},
new[]
{
new[] { "a", "b", "c" },
new[] {
new Constant(5),
new Constant(6),
new Constant(7),
new Constant(7)
}
);
@ -34,13 +32,11 @@ namespace AspectedRouting.Test
public void ToLua_NestedMapping_Table()
{
var mapping = new Mapping(
new[] {"a"},
new[]
{
new Mapping(new[] {"b"},
new[]
{
new Constant(42),
new[] { "a" },
new[] {
new Mapping(new[] { "b" },
new[] {
new Constant(42)
}
)
}
@ -54,10 +50,8 @@ namespace AspectedRouting.Test
public void Sanity_EveryBasicFunction_HasDescription()
{
var missing = new List<string>();
foreach (var (_, f) in Funcs.Builtins)
{
if (string.IsNullOrEmpty(f.Description))
{
foreach (var (_, f) in Funcs.Builtins) {
if (string.IsNullOrEmpty(f.Description)) {
missing.Add(f.Name);
}
}
@ -65,15 +59,13 @@ namespace AspectedRouting.Test
Assert.True(0 == missing.Count,
"These functions do not have a description: " + string.Join(", ", missing));
}
[Fact]
public void Sanity_EveryBasicFunction_HasArgNames()
{
var missing = new List<string>();
foreach (var (_, f) in Funcs.Builtins)
{
if (f.ArgNames == null)
{
foreach (var (_, f) in Funcs.Builtins) {
if (f.ArgNames == null) {
missing.Add(f.Name);
}
}

View file

@ -9,18 +9,18 @@ namespace AspectedRouting.Test
[Fact]
public static void SimpleMapping_SimpleHighway_GivesResult()
{
var maxspeed = new Mapping(new[] {"residential", "living_street"},
var maxspeed = new Mapping(new[] { "residential", "living_street" },
new[] {
new Constant(30),
new Constant(20)
}
);
var resMaxspeed= maxspeed.Evaluate(new Context(), new Constant("residential"));
Assert.Equal(30, resMaxspeed);
var livingStreetMaxspeed= maxspeed.Evaluate(new Context(), new Constant("living_street"));
Assert.Equal(20, livingStreetMaxspeed);
var undefinedSpeed = maxspeed.Evaluate(new Context(), new Constant("some_unknown_highway_type"));
Assert.Null(undefinedSpeed);
var resMaxspeed = maxspeed.Evaluate(new Context(), new Constant("residential"));
Assert.Equal(30, resMaxspeed);
var livingStreetMaxspeed = maxspeed.Evaluate(new Context(), new Constant("living_street"));
Assert.Equal(20, livingStreetMaxspeed);
var undefinedSpeed = maxspeed.Evaluate(new Context(), new Constant("some_unknown_highway_type"));
Assert.Null(undefinedSpeed);
}
}
}

View file

@ -10,37 +10,37 @@ namespace AspectedRouting.Test
[Fact]
public void MustMatch_SimpleInput()
{
var mapValue = new Mapping(new[] {"residential", "living_street"},
var mapValue = new Mapping(new[] { "residential", "living_street" },
new[] {
new Constant("yes"),
new Constant("no")
});
var mapTag = new Mapping(new[] {"highway"}, new[] {mapValue});
var mapTag = new Mapping(new[] { "highway" }, new[] { mapValue });
var mm = Funcs.MustMatch
.Apply(
new Constant(new[] {new Constant("highway")}),
new Constant(new[] { new Constant("highway") }),
Funcs.StringStringToTags.Apply(mapTag)
)
;
var residential = mm.Apply(new Constant(new Dictionary<string, string> {
{"highway", "residential"}
{ "highway", "residential" }
})).Evaluate(new Context());
Assert.Equal("yes", residential);
var living = mm.Apply(new Constant(new Dictionary<string, string> {
{"highway", "living_street"}
{ "highway", "living_street" }
})).Evaluate(new Context());
Assert.Equal("no", living);
var unknown = mm.Apply(new Constant(new Dictionary<string, string> {
{"highway", "unknown_type"}
{ "highway", "unknown_type" }
})).Evaluate(new Context());
Assert.Equal("yes", unknown);
var missing = mm.Apply(new Constant(new Dictionary<string, string> {
{"proposed:highway", "unknown_type"}
{ "proposed:highway", "unknown_type" }
})).Evaluate(new Context());
Assert.Equal("no", missing);
}

View file

@ -43,7 +43,7 @@ namespace AspectedRouting.Test.Snippets
var func = new Apply(
Funcs.StringStringToTags,
new Mapping(
new[] {"bicycle", "access"},
new[] { "bicycle", "access" },
new IExpression[] {
Funcs.Id,
Funcs.Id
@ -51,7 +51,7 @@ namespace AspectedRouting.Test.Snippets
)
);
var tags = new LuaLiteral(new[] {Typs.Tags}, "tags");
var tags = new LuaLiteral(new[] { Typs.Tags }, "tags");
var code = gen.Convert(lua, "result",
new List<IExpression> {
@ -71,7 +71,7 @@ namespace AspectedRouting.Test.Snippets
public void SimpleMappingSnippet_SimpleMapping_GeneratesLua()
{
var mapping = new Mapping(
new[] {"1", "-1"},
new[] { "1", "-1" },
new IExpression[] {
new Constant("with"),
new Constant("against")
@ -86,6 +86,5 @@ namespace AspectedRouting.Test.Snippets
"local v\nv = tags.oneway\n\nif (v == \"1\") then\n result = \"with\"\nelseif (v == \"-1\") then\n result = \"against\"\nend";
Assert.Equal(expected, code);
}
}
}

View file

@ -18,11 +18,10 @@ namespace AspectedRouting.Test
"{\"name\": \"legal_maxspeed_be\",\"description\": \"Gives, for each type of highway, which the default legal maxspeed is in Belgium. This file is intended to be reused for in all vehicles, from pedestrian to car. In some cases, a legal maxspeed is not really defined (e.g. on footways). In that case, a socially acceptable speed should be taken (e.g.: a bicycle on a pedestrian path will go say around 12km/h)\",\"unit\": \"km/h\",\"$max\": {\"maxspeed\": \"$parse\",\"highway\": {\"residential\": 30},\"ferry\":5}}";
var aspect = JsonParser.AspectFromJson(null, json, null);
var tags = new Dictionary<string, string>
{
{"maxspeed", "42"},
{"highway", "residential"},
{"ferry", "yes"}
var tags = new Dictionary<string, string> {
{ "maxspeed", "42" },
{ "highway", "residential" },
{ "ferry", "yes" }
};
Assert.Equal("tags -> pdouble", string.Join(", ", aspect.Types));
@ -40,11 +39,10 @@ namespace AspectedRouting.Test
var aspect = JsonParser.AspectFromJson(null, json, null);
Assert.Equal(
new Dictionary<string, HashSet<string>>
{
{"maxspeed", new HashSet<string>()},
{"highway", new HashSet<string> {"residential"}},
{"ferry", new HashSet<string>()}
new Dictionary<string, HashSet<string>> {
{ "maxspeed", new HashSet<string>() },
{ "highway", new HashSet<string> { "residential" } },
{ "ferry", new HashSet<string>() }
},
aspect.PossibleTags());
}
@ -79,7 +77,7 @@ namespace AspectedRouting.Test
public void EitherFunc_SpecializeToString_Const()
{
var a = new Constant("a");
var mconst = new Apply(new Apply(Funcs.EitherFunc, Funcs.Id), Funcs.Const);
var specialized = new Apply(mconst, a).Specialize(Typs.String);
@ -125,7 +123,7 @@ namespace AspectedRouting.Test
public void MaxTest()
{
var ls = new Constant(new ListType(Typs.Double),
new[] {1.1, 2.0, 3.0}.Select(d => (object) d));
new[] { 1.1, 2.0, 3.0 }.Select(d => (object)d));
Assert.Equal("[1.1, 2, 3] : list (double)",
ls.Evaluate(null).Pretty() + " : " + string.Join(", ", ls.Types));
var mx = Funcs.Max.Apply(ls);
@ -164,7 +162,7 @@ namespace AspectedRouting.Test
[Fact]
public void TestStringGeneration()
{
var v = Var.Fresh(new HashSet<string> {"$a", "$b"});
var v = Var.Fresh(new HashSet<string> { "$a", "$b" });
Assert.Equal("$c", v.Name);
}
@ -176,9 +174,9 @@ namespace AspectedRouting.Test
var app = new Apply(
new Apply(
new Apply(Funcs.Id, Funcs.Id), Funcs.Id), a);
var (f, args ) = app.DeconstructApply().Value;
Assert.Equal(Funcs.Id.Name, ((Function) f).Name);
Assert.Equal(new List<IExpression> {Funcs.Id, Funcs.Id, a}.Select(e => e.ToString()),
var (f, args) = app.DeconstructApply().Value;
Assert.Equal(Funcs.Id.Name, ((Function)f).Name);
Assert.Equal(new List<IExpression> { Funcs.Id, Funcs.Id, a }.Select(e => e.ToString()),
args.Select(e => e.ToString()));
}

View file

@ -52,8 +52,8 @@ namespace AspectedRouting.Test
new Curry(Typs.Tags, Typs.Double), x
);
}
[Fact]
public void WidestCommonGround_StringAndString_String()
{

View file

@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/PencilsConfiguration/ActualSeverity/@EntryValue">ERROR</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=3112587d_002D1c06_002D4ad3_002Da845_002D73105d4b723a/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="DefaultSnippet_SimpleDefault_GetsLua" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
&lt;TestAncestor&gt;
&lt;TestId&gt;xUnit::A1309041-8AAE-42D7-A886-94C9FFC6A28C::.NETCoreApp,Version=v3.1::AspectedRouting.Test.Snippets.SnippetTests&lt;/TestId&gt;

View file

@ -13,7 +13,7 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Examples\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

View file

@ -9,19 +9,17 @@ namespace AspectedRouting.IO.LuaSkeleton
public class LuaLiteral : IExpression
{
public readonly string Lua;
public IEnumerable<Type> Types { get; }
public LuaLiteral(Type type, string lua):this(new [] {type}, lua)
{
}
public LuaLiteral(Type type, string lua) : this(new[] { type }, lua) { }
public LuaLiteral(IEnumerable<Type> types, string lua)
{
Lua = lua;
Types = types;
}
public IEnumerable<Type> Types { get; }
public object Evaluate(Context c, params IExpression[] arguments)
{
throw new NotImplementedException();
@ -32,11 +30,11 @@ namespace AspectedRouting.IO.LuaSkeleton
return this;
}
public IExpression PruneTypes(Func<Type, bool> allowedTypes)
public IExpression PruneTypes(System.Func<Type, bool> allowedTypes)
{
var passed = this.Types.Where(allowedTypes);
var passed = Types.Where(allowedTypes);
if (passed.Any()) {
return new LuaLiteral(passed, this.Lua);
return new LuaLiteral(passed, Lua);
}
return null;
@ -44,13 +42,12 @@ namespace AspectedRouting.IO.LuaSkeleton
public IExpression Optimize()
{
return this;
return this;
}
public void Visit(Func<IExpression, bool> f)
{
throw new NotImplementedException();
}
}
}

View file

@ -6,8 +6,8 @@ namespace AspectedRouting.IO.itinero1
{
public class LuaParameterPrinter
{
private ProfileMetaData _profile;
private LuaSkeleton.LuaSkeleton _skeleton;
private readonly ProfileMetaData _profile;
private readonly LuaSkeleton.LuaSkeleton _skeleton;
public LuaParameterPrinter(ProfileMetaData profile, LuaSkeleton.LuaSkeleton skeleton)
{
@ -18,8 +18,7 @@ namespace AspectedRouting.IO.itinero1
public string GenerateDefaultParameters()
{
var impl = new List<string>()
{
var impl = new List<string> {
"function default_parameters()",
" local parameters = {}",
DeclareParametersFor(_profile.DefaultParameters),
@ -31,34 +30,26 @@ namespace AspectedRouting.IO.itinero1
}
/// <summary>
/// Generates a piece of code of the following format:
///
/// parameters["x"] = a;
/// parameters["y"] = b:
/// ...
///
/// Where x=a and y=b are defined in the profile
///
/// Dependencies are added.
///
/// Note that the caller should still add `local paramaters = default_parameters()`
///
/// Generates a piece of code of the following format:
/// parameters["x"] = a;
/// parameters["y"] = b:
/// ...
/// Where x=a and y=b are defined in the profile
/// Dependencies are added.
/// Note that the caller should still add `local paramaters = default_parameters()`
/// </summary>
/// <param name="behaviour"></param>
/// <returns></returns>
public string DeclareParametersFor(Dictionary<string, IExpression> subParams)
{
var impl = "";
foreach (var (paramName, value) in subParams)
{
if (paramName.Equals("description"))
{
foreach (var (paramName, value) in subParams) {
if (paramName.Equals("description")) {
continue;
}
var paramNameTrimmed = paramName.TrimStart('#').AsLuaIdentifier();
if (!string.IsNullOrEmpty(paramNameTrimmed))
{
if (!string.IsNullOrEmpty(paramNameTrimmed)) {
impl += $" parameters.{paramNameTrimmed} = {_skeleton.ToLua(value)}\n";
}
}

View file

@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Tests;
namespace AspectedRouting.IO.md
{
internal class MarkDownSection
{
private readonly List<string> parts = new List<string>();
public string ToString()
{
return string.Join("\n\n", parts);
}
public void AddTitle(string title, int level)
{
var str = "";
for (var i = 0; i < level; i++) {
str += "#";
}
str += " " + title;
parts.Add(str);
}
public void Add(params string[] paragraph)
{
parts.Add(string.Join("\n", paragraph));
}
public void AddList(List<string> items)
{
parts.Add(string.Join("\n", items.Select(i => " - " + i)));
}
}
public class ProfileToMD
{
private readonly string _behaviour;
private readonly Context _c;
private readonly ProfileMetaData _profile;
private readonly MarkDownSection md = new MarkDownSection();
public ProfileToMD(ProfileMetaData profile, string behaviour, Context c)
{
_profile = profile;
_behaviour = behaviour;
_c = c.WithAspectName(behaviour);
_c.DefinedFunctions["speed"] = new AspectMetadata(profile.Speed, "speed", "The speed this vehicle is going",
"", "km/h", "", true);
if (!profile.Behaviours.ContainsKey(behaviour)) {
throw new ArgumentException("Profile does not contain behaviour " + behaviour);
}
}
private decimal R(double d)
{
return Math.Round((decimal)d, 2);
}
/**
* Calculates an entry with `speed`, `priority` for the profile
*/
public string TableEntry(string msg, Dictionary<string, string> tags, ProfileResult? reference,
bool nullOnSame = false)
{
var profile = _profile.Run(_c, _behaviour, tags);
if (!reference.HasValue) {
return "| " + msg + " | " + profile.Speed + " | " + profile.Priority + " | ";
}
if (reference.Equals(profile) && nullOnSame) {
return null;
}
return "| " + msg + " | " + R(profile.Speed) + " | " +
R(profile.Speed / reference.Value.Speed) + " | " +
R(profile.Priority) + " | " + R(profile.Priority / reference.Value.Priority) + " | " +
profile.Access + " | " + profile.Oneway;
}
public void addTagsTable(ProfileResult reference, Dictionary<string, HashSet<string>> usedTags)
{
var p = _profile;
var b = _profile.Behaviours[_behaviour];
var tableEntries = new List<string>();
foreach (var (key, vals) in usedTags) {
var values = vals;
if (values.Count == 0 && key == "maxspeed") {
tableEntries.Add($" | {key}=* (example values below)");
values = new HashSet<string> {
"20", "30", "50", "70", "90", "120", "150"
};
}
if (values.Count == 0) {
tableEntries.Add($" | {key}=*");
}
if (values.Count > 0) {
foreach (var value in values) {
var tags = new Dictionary<string, string> {
[key] = value
};
var entry = TableEntry($"{key}={value} ", tags, reference);
if (entry == null) {
continue;
}
tableEntries.Add(entry);
}
}
}
md.Add("| Tags | Speed (km/h) | speedfactor | Priority | priorityfactor | access | oneway | ",
"| ---- | ------------ | ----------- | -------- | --------------- | ----- | ------ |",
string.Join("\n", tableEntries));
}
public Dictionary<string, IExpression> TagsWithPriorityInfluence()
{
var p = _profile;
var parameters = _profile.ParametersFor(_behaviour);
var withInfluence = new Dictionary<string, IExpression>();
foreach (var kv in p.Priority) {
if (parameters[kv.Key].Equals(0.0) || parameters[kv.Key].Equals(0)) {
continue;
}
withInfluence[kv.Key] = kv.Value;
}
return withInfluence;
}
public string MainFormula()
{
var p = _profile;
var b = _profile.Behaviours[_behaviour];
var overridenParams = new HashSet<string>();
var paramValues = new Dictionary<string, object>();
foreach (var kv in p.DefaultParameters) {
paramValues[kv.Key] = kv.Value.Evaluate(_c);
}
foreach (var kv in b) {
paramValues[kv.Key] = kv.Value.Evaluate(_c);
overridenParams.Add(kv.Key);
}
var mainFormulaParts = p.Priority.Select(delegate(KeyValuePair<string, IExpression> kv) {
var key = kv.Key;
var param = paramValues[key];
if (param.Equals(0) || param.Equals(0.0)) {
return "";
}
if (overridenParams.Contains(key)) {
param = "**" + param + "**";
}
var called = kv.Value.DirectlyCalled();
return param + " * `" + string.Join("", called.calledFunctionNames) + "`";
});
var mainFormula = string.Join(" + ", mainFormulaParts.Where(p => p != ""));
return mainFormula;
}
public override string ToString()
{
var p = _profile;
var b = _profile.Behaviours[_behaviour];
md.AddTitle(_profile.Name + "." + _behaviour, 1);
md.Add(p.Description);
if (b.ContainsKey("description")) {
md.Add(b["description"].Evaluate(_c).ToString());
}
md.Add("This profile is calculated as following (non-default keys are bold):", MainFormula());
var residentialTags = new Dictionary<string, string> {
["highway"] = "residential"
};
md.Add("| Tags | Speed (km/h) | Priority",
"| ---- | ----- | ---------- | ",
TableEntry("Residential highway (reference)", residentialTags, null));
var reference = _profile.Run(_c, _behaviour, residentialTags);
md.AddTitle("Tags influencing priority", 2);
md.Add(
"Priority is what influences which road to take. The routeplanner will search a way where `1/priority` is minimal.");
addTagsTable(reference, TagsWithPriorityInfluence().Values.PossibleTagsRecursive(_c));
md.AddTitle("Tags influencing speed", 2);
md.Add(
"Speed is used to calculate how long the trip will take, but does _not_ influence which route is taken. Some profiles do use speed as a factor in priority too - in this case, these tags will be mentioned above too.");
addTagsTable(reference, _profile.Speed.PossibleTagsRecursive(_c));
md.AddTitle("Tags influencing access", 2);
md.Add("These tags influence whether or not this road can be taken with this vehicle or behaviour");
addTagsTable(reference, _profile.Access.PossibleTagsRecursive(_c));
md.AddTitle("Tags influencing oneway", 2);
md.Add("These tags influence whether or not this road can be taken in all directions or not");
addTagsTable(reference, _profile.Oneway.PossibleTagsRecursive(_c));
return md.ToString();
}
}
}

View file

@ -2,7 +2,8 @@
## Introduction
Generating a good route for travellers is hard; especially for cyclists. They can be very picky and the driving style and purposes are diverse. Think about:
Generating a good route for travellers is hard; especially for cyclists. They can be very picky and the driving style
and purposes are diverse. Think about:
- A lightweight food delivery driver, who wants to be at their destination as soon as possible
- A cargo bike, possibly with a cart or electrically supported, doing heavy delivery
@ -10,13 +11,18 @@ Generating a good route for travellers is hard; especially for cyclists. They ca
- Grandma cycling along a canal on sunday afternoon
- Someone bringing their kids to school
It is clear that these persona's on top have very different wishes for their route. A road with a high car pressure won't pose a problem for the food delivery, whereas grandma wouldn't even think about going there. And this is without mentioning the speed these cyclists drive, where they are allowed to drive, ...
It is clear that these persona's on top have very different wishes for their route. A road with a high car pressure
won't pose a problem for the food delivery, whereas grandma wouldn't even think about going there. And this is without
mentioning the speed these cyclists drive, where they are allowed to drive, ...
Generating a cycle route for these persons is thus clearly far from simply picking the shortest possible path. On top of that, a consumer expects the route calculations to be both customizable and to be blazingly fast.
Generating a cycle route for these persons is thus clearly far from simply picking the shortest possible path. On top of
that, a consumer expects the route calculations to be both customizable and to be blazingly fast.
In order to simplify the generation of these routing profiles, this repository introduces _aspected routing_.
In _aspected routing_, one does not try to tackle the routing problem all at once, but one tries to dissassemble the preferences of the travellers into multiple, orthogonal aspects. These aspects can then be combined in a linear way, giving a fast and flexible system.
In _aspected routing_, one does not try to tackle the routing problem all at once, but one tries to dissassemble the
preferences of the travellers into multiple, orthogonal aspects. These aspects can then be combined in a linear way,
giving a fast and flexible system.
Some aspects can be:
@ -38,9 +44,12 @@ Even though this repository is heavily inspired on OpenStreetMap, it can be gene
## Road network assumptions
The only assumptions made are that roads have a **length** and a collection of **tags**, this is a dictionary mapping strings onto strings. These tags encode the properties of the road (e.g. road classification, name, surface, ...)
The only assumptions made are that roads have a **length** and a collection of **tags**, this is a dictionary mapping
strings onto strings. These tags encode the properties of the road (e.g. road classification, name, surface, ...)
OpenStreetMap also has a concept of **relations**. A special function is available for that. However, in a preprocessing step, the relations that a road is a member of, are converted into tags on every way with a `_network:i:key=value` format, where `i` is the number of the relation, and `key`=`value` is a tag present on the relation.
OpenStreetMap also has a concept of **relations**. A special function is available for that. However, in a preprocessing
step, the relations that a road is a member of, are converted into tags on every way with a `_network:i:key=value`
format, where `i` is the number of the relation, and `key`=`value` is a tag present on the relation.
## Describing an aspect

View file

@ -7,6 +7,7 @@
- string
- tags
- bool
## Builtin functions
- eq
@ -37,7 +38,6 @@
- eitherFunc
- stringToTags
### Function overview
#### eq
@ -49,8 +49,6 @@ $a | $a | string |
Returns 'yes' if both values _are_ the same
Lua implementation:
````lua
@ -64,7 +62,6 @@ end
````
#### notEq
a | b | returns |
@ -75,8 +72,6 @@ bool | bool |
OVerloaded function, either boolean not or returns 'yes' if the two passed in values are _not_ the same;
Lua implementation:
````lua
@ -93,7 +88,6 @@ function notEq(a, b)
end
````
#### not
a | b | returns |
@ -104,8 +98,6 @@ bool | bool |
OVerloaded function, either boolean not or returns 'yes' if the two passed in values are _not_ the same;
Lua implementation:
````lua
@ -122,7 +114,6 @@ function notEq(a, b)
end
````
#### inv
d | returns |
@ -132,8 +123,6 @@ double | double |
Calculates `1/d`
Lua implementation:
````lua
@ -142,7 +131,6 @@ function inv(n)
end
````
#### default
defaultValue | f | returns |
@ -151,8 +139,6 @@ $a | $b -> $a | $b | $a |
Calculates function `f` for the given argument. If the result is `null`, the default value is returned instead
Lua implementation:
````lua
@ -164,7 +150,6 @@ function default(defaultValue, realValue)
end
````
#### parse
s | returns |
@ -174,8 +159,6 @@ string | pdouble |
Parses a string into a numerical value
Lua implementation:
````lua
@ -208,7 +191,6 @@ function parse(string)
end
````
#### to_string
obj | returns |
@ -217,8 +199,6 @@ $a | string |
Converts a value into a human readable string
Lua implementation:
````lua
@ -227,7 +207,6 @@ function to_string(o)
end
````
#### concat
a | b | returns |
@ -236,8 +215,6 @@ string | string | string |
Concatenates two strings
Lua implementation:
````lua
@ -246,7 +223,6 @@ function concat(a, b)
end
````
#### containedIn
list | a | returns |
@ -255,8 +231,6 @@ list ($a) | $a | bool |
Given a list of values, checks if the argument is contained in the list.
Lua implementation:
````lua
@ -271,7 +245,6 @@ function containedIn(list, a)
end
````
#### min
list | returns |
@ -284,8 +257,6 @@ list (bool) | bool |
Out of a list of values, gets the smallest value. IN case of a list of bools, this acts as `and`
Lua implementation:
````lua
@ -303,7 +274,6 @@ function min(list)
end
````
#### and
list | returns |
@ -316,8 +286,6 @@ list (bool) | bool |
Out of a list of values, gets the smallest value. IN case of a list of bools, this acts as `and`
Lua implementation:
````lua
@ -335,7 +303,6 @@ function min(list)
end
````
#### max
list | returns |
@ -348,8 +315,6 @@ list (bool) | bool |
Returns the biggest value in the list. For a list of booleans, this acts as 'or'
Lua implementation:
````lua
@ -367,7 +332,6 @@ function max(list)
end
````
#### or
list | returns |
@ -380,8 +344,6 @@ list (bool) | bool |
Returns the biggest value in the list. For a list of booleans, this acts as 'or'
Lua implementation:
````lua
@ -399,7 +361,6 @@ function max(list)
end
````
#### sum
list | returns |
@ -412,8 +373,6 @@ list (bool) | int |
Sums all the numbers in the given list. If the list contains bool, `yes` or `true` will be considered to equal `1`
Lua implementation:
````lua
@ -429,7 +388,6 @@ function sum(list)
end
````
#### multiply
list | returns |
@ -442,8 +400,6 @@ list (bool) | bool |
Multiplies all the values in a given list. On a list of booleans, this acts as 'and' or 'all'
Lua implementation:
````lua
@ -456,7 +412,6 @@ function multiply(list)
end
````
#### firstMatchOf
s | returns |
@ -465,8 +420,6 @@ list (string) | tags -> list ($a) | tags | $a |
Parses a string into a numerical value
Lua implementation:
````lua
@ -492,18 +445,15 @@ function first_match_of(tags, result, order_of_keys, table)
end
````
#### mustMatch
neededKeys (filled in by parser) | f | returns |
--- | --- | --- |
list (string) | tags -> list (bool) | tags | bool |
Every key that is used in the subfunction must be present.
If, on top, a value is present with a mapping, every key/value will be executed and must return a value that is not 'no' or 'false'
Note that this is a privileged builtin function, as the parser will automatically inject the keys used in the called function.
Every key that is used in the subfunction must be present. If, on top, a value is present with a mapping, every
key/value will be executed and must return a value that is not 'no' or 'false' Note that this is a privileged builtin
function, as the parser will automatically inject the keys used in the called function.
Lua implementation:
@ -572,7 +522,6 @@ function must_match(tags, result, needed_keys, table)
end
````
#### memberOf
f | tags | returns |
@ -586,15 +535,18 @@ In order to use this for itinero 1.0, the membership _must_ be the top level exp
Conceptually, when the aspect is executed for a way, every relation will be used as argument in the subfunction `f`
If this subfunction returns 'true', the entire aspect will return true.
In the lua implementation for itinero 1.0, this is implemented slightly different: a flag `_relation:<aspect_name>="yes"` will be set if the aspect matches on every way for where this aspect matches.
However, this plays poorly with parameters (e.g.: what if we want to cycle over a highway which is part of a certain cycling network with a certain `#network_name`?) Luckily, parameters can only be simple values. To work around this problem, an extra tag is introduced for _every single profile_:`_relation:<profile_name>:<aspect_name>=yes'. The subfunction is thus executed `countOr(relations) * countOf(profiles)` time, yielding `countOf(profiles)` tags. The profile function then picks the tags for himself and strips the `<profile_name>:` away from the key.
In the lua implementation for itinero 1.0, this is implemented slightly different: a
flag `_relation:<aspect_name>="yes"` will be set if the aspect matches on every way for where this aspect matches.
However, this plays poorly with parameters (e.g.: what if we want to cycle over a highway which is part of a certain
cycling network with a certain `#network_name`?) Luckily, parameters can only be simple values. To work around this
problem, an extra tag is introduced for _every single
profile_:`_relation:<profile_name>:<aspect_name>=yes'. The subfunction is thus executed `countOr(relations) * countOf(
profiles)` time, yielding `countOf(
profiles)` tags. The profile function then picks the tags for himself and strips the `<profile_name>:` away from the
key.
In the test.csv, one can simply use `_relation:<aspect_name>=yes` to mimic relations in your tests
Lua implementation:
````lua
@ -610,7 +562,6 @@ function member_of(calledIn, parameters, tags, result)
end
````
#### if_then_else
condition | then | else | returns |
@ -618,9 +569,8 @@ condition | then | else | returns |
bool | $a | $a | $a |
bool | $a | $a |
Selects either one of the branches, depending on the condition.If the `else` branch is not set, `null` is returned in the condition is false.
Selects either one of the branches, depending on the condition.If the `else` branch is not set, `null` is returned in
the condition is false.
Lua implementation:
@ -634,7 +584,6 @@ function if_then_else(condition, thn, els)
end
````
#### if
condition | then | else | returns |
@ -642,9 +591,8 @@ condition | then | else | returns |
bool | $a | $a | $a |
bool | $a | $a |
Selects either one of the branches, depending on the condition.If the `else` branch is not set, `null` is returned in the condition is false.
Selects either one of the branches, depending on the condition.If the `else` branch is not set, `null` is returned in
the condition is false.
Lua implementation:
@ -658,7 +606,6 @@ function if_then_else(condition, thn, els)
end
````
#### id
a | returns |
@ -667,8 +614,6 @@ $a | $a |
Returns the argument unchanged - the identity function. Seems useless at first sight, but useful in parsing
Lua implementation:
````lua
@ -677,7 +622,6 @@ function id(v)
end
````
#### const
a | b | returns |
@ -686,8 +630,6 @@ $a | $b | $a |
Small utility function, which takes two arguments `a` and `b` and returns `a`. Used extensively to insert freedom
Lua implementation:
````lua
@ -696,7 +638,6 @@ function const(a, b)
end
````
#### constRight
a | b | returns |
@ -705,24 +646,20 @@ $a | $b | $b |
Small utility function, which takes two arguments `a` and `b` and returns `b`. Used extensively to insert freedom
Lua implementation:
````lua
````
#### dot
f | g | a | returns |
--- | --- | --- | --- |
$b -> $c | $a -> $b | $a | $c |
Higher order function: converts `f (g a)` into `(dot f g) a`. In other words, this fuses `f` and `g` in a new function, which allows the argument to be lifted out of the expression
Higher order function: converts `f (g a)` into `(dot f g) a`. In other words, this fuses `f` and `g` in a new function,
which allows the argument to be lifted out of the expression
Lua implementation:
@ -730,16 +667,14 @@ Lua implementation:
````
#### listDot
list | a | returns |
--- | --- | --- |
list ($a -> $b) | $a | list ($b) |
Listdot takes a list of functions `[f, g, h]` and and an argument `a`. It applies the argument on every single function.It conveniently lifts the argument out of the list.
Listdot takes a list of functions `[f, g, h]` and and an argument `a`. It applies the argument on every single
function.It conveniently lifts the argument out of the list.
Lua implementation:
@ -748,7 +683,6 @@ Lua implementation:
-- listDot
````
#### eitherFunc
f | g | a | returns |
@ -756,21 +690,24 @@ f | g | a | returns |
$a -> $b | $c -> $d | $a | $b |
$a -> $b | $c -> $d | $c | $d |
EitherFunc is a small utility function, mostly used in the parser. It allows the compiler to choose a function, based on the types.
EitherFunc is a small utility function, mostly used in the parser. It allows the compiler to choose a function, based on
the types.
Consider the mapping `{'someKey':'someValue'}`. Under normal circumstances, this acts as a pointwise-function, converting the string `someKey` into `someValue`, just like an ordinary dictionary would do. However, in the context of `mustMatch`, we would prefer this to act as a _check_, that the highway _has_ a key `someKey` which is `someValue`, thus acting as `{'someKey': {'$eq':'someValue'}}. Both behaviours are automatically supported in parsing, by parsing the string as `(eitherFunc id eq) 'someValue'`. The type system is then able to figure out which implementation is needed.
Consider the mapping `{'someKey':'someValue'}`. Under normal circumstances, this acts as a pointwise-function,
converting the string `someKey` into `someValue`, just like an ordinary dictionary would do. However, in the context
of `mustMatch`, we would prefer this to act as a _check_, that the highway _has_ a key `someKey` which is `someValue`,
thus acting
as `{'someKey': {'$eq':'someValue'}}. Both behaviours are automatically supported in parsing, by parsing the string as `(
eitherFunc id eq) 'someValue'`. The type system is then able to figure out which implementation is needed.
Disclaimer: _you should never ever need this in your profiles_
Lua implementation:
````lua
````
#### stringToTags
f | tags | returns |
@ -779,8 +716,6 @@ string -> string -> $a | tags | list ($a) |
stringToTags converts a function `string -> string -> a` into a function `tags -> [a]`
Lua implementation:
````lua

View file

@ -10,7 +10,6 @@ namespace AspectedRouting.Language
{
public static class Analysis
{
public static Dictionary<string, (List<Type> Types, string inFunction)> UsedParameters(
this ProfileMetaData profile, Context context)
{
@ -20,23 +19,19 @@ namespace AspectedRouting.Language
void AddParams(IExpression e, string inFunction)
{
var parms = e.UsedParameters();
foreach (var param in parms)
{
if (parameters.TryGetValue(param.ParamName, out var typesOldUsage))
{
foreach (var param in parms) {
if (parameters.TryGetValue(param.ParamName, out var typesOldUsage)) {
var (types, oldUsage) = typesOldUsage;
var unified = types.SpecializeTo(param.Types);
if (unified == null)
{
if (unified == null) {
throw new ArgumentException("Inconsistent parameter usage: the paremeter " +
param.ParamName + " is used\n" +
$" in {oldUsage} as {string.Join(",", types)}\n" +
$" in {inFunction} as {string.Join(",", param.Types)}\n" +
$"which can not be unified");
"which can not be unified");
}
}
else
{
else {
parameters[param.ParamName] = (param.Types.ToList(), inFunction);
}
}
@ -47,19 +42,16 @@ namespace AspectedRouting.Language
AddParams(profile.Oneway, "profile definition for " + profile.Name + ".oneway");
AddParams(profile.Speed, "profile definition for " + profile.Name + ".speed");
foreach (var (key, expr) in profile.Priority)
{
foreach (var (key, expr) in profile.Priority) {
AddParams(new Parameter(key), profile.Name + ".priority.lefthand");
AddParams(expr, profile.Name + ".priority");
}
var calledFunctions = profile.CalledFunctionsRecursive(context).Values
.SelectMany(ls => ls).ToHashSet();
foreach (var calledFunction in calledFunctions)
{
foreach (var calledFunction in calledFunctions) {
var func = context.GetFunction(calledFunction);
if (func is AspectMetadata meta && meta.ProfileInternal)
{
if (func is AspectMetadata meta && meta.ProfileInternal) {
continue;
}
@ -74,10 +66,8 @@ namespace AspectedRouting.Language
public static HashSet<Parameter> UsedParameters(this IExpression e)
{
var result = new HashSet<Parameter>();
e.Visit(expr =>
{
if (expr is Parameter p)
{
e.Visit(expr => {
if (expr is Parameter p) {
result.Add(p);
}
@ -98,18 +88,14 @@ namespace AspectedRouting.Language
void ScanExpression(IExpression e, string inFunction)
{
if (!result.ContainsKey(inFunction))
{
if (!result.ContainsKey(inFunction)) {
result.Add(inFunction, new List<string>());
}
e.Visit(x =>
{
if (x is FunctionCall fc)
{
e.Visit(x => {
if (x is FunctionCall fc) {
result[inFunction].Add(fc.CalledFunctionName);
if (!result.ContainsKey(fc.CalledFunctionName))
{
if (!result.ContainsKey(fc.CalledFunctionName)) {
calledFunctions.Enqueue(fc.CalledFunctionName);
}
}
@ -123,14 +109,12 @@ namespace AspectedRouting.Language
ScanExpression(profile.Oneway, profile.Name + ".oneway");
ScanExpression(profile.Speed, profile.Name + ".speed");
foreach (var (key, expr) in profile.Priority)
{
foreach (var (key, expr) in profile.Priority) {
ScanExpression(new Parameter(key), $"{profile.Name}.priority.{key}.lefthand");
ScanExpression(expr, $"{profile.Name}.priority.{key}");
}
while (calledFunctions.TryDequeue(out var calledFunction))
{
while (calledFunctions.TryDequeue(out var calledFunction)) {
var func = c.GetFunction(calledFunction);
ScanExpression(func, calledFunction);
}
@ -148,16 +132,14 @@ namespace AspectedRouting.Language
var queue = new Queue<IExpression>();
exprs.ForEach(queue.Enqueue);
while (queue.TryDequeue(out var next))
{
while (queue.TryDequeue(out var next)) {
var (p, deps) = next.DirectlyCalled();
parameters.UnionWith(p);
var toCheck = deps.Except(dependencies);
dependencies.UnionWith(deps);
foreach (var fName in toCheck)
{
queue.Enqueue(ctx.GetFunction(fName));
foreach (var fName in toCheck) {
queue.Enqueue(ctx.GetFunction(fName));
}
}
@ -166,7 +148,8 @@ namespace AspectedRouting.Language
/// <summary>
/// Generates an overview of the dependencies of the expression, both which parameters it needs and what other functions (builtin or defined) it needs.
/// Generates an overview of the dependencies of the expression, both which parameters it needs and what other
/// functions (builtin or defined) it needs.
/// </summary>
/// <param name="expr"></param>
/// <returns></returns>
@ -176,15 +159,12 @@ namespace AspectedRouting.Language
var parameters = new HashSet<string>();
var dependencies = new HashSet<string>();
expr.Visit(e =>
{
if (e is FunctionCall fc)
{
expr.Visit(e => {
if (e is FunctionCall fc) {
dependencies.Add(fc.CalledFunctionName);
}
if (e is Parameter p)
{
if (e is Parameter p) {
parameters.Add(p.ParamName);
}
@ -197,8 +177,7 @@ namespace AspectedRouting.Language
public static string TypeBreakdown(this IExpression e)
{
return e.ToString() + " : "+string.Join(" ; ", e.Types);
return e + " : " + string.Join(" ; ", e.Types);
}
public static void SanityCheckProfile(this ProfileMetaData pmd, Context context)
@ -212,8 +191,7 @@ namespace AspectedRouting.Language
string MetaList(IEnumerable<string> paramNames)
{
var metaInfo = "";
foreach (var paramName in paramNames)
{
foreach (var paramName in paramNames) {
var _ = usedMetadata.TryGetValue(paramName, out var inFunction) ||
usedMetadata.TryGetValue('#' + paramName, out inFunction);
metaInfo += $"\n - {paramName} (used in {inFunction.inFunction})";
@ -225,43 +203,36 @@ namespace AspectedRouting.Language
var usedParameters = usedMetadata.Keys.Select(key => key.TrimStart('#')).ToList();
var diff = usedParameters.ToHashSet().Except(defaultParameters).ToList();
if (diff.Any())
{
if (diff.Any()) {
throw new ArgumentException("No default value set for parameter: " + MetaList(diff));
}
var unused = defaultParameters.Except(usedParameters);
if (unused.Any())
{
if (unused.Any()) {
Console.WriteLine("[WARNING] A default value is set for parameter, but it is unused: " +
string.Join(", ", unused));
string.Join(", ", unused));
}
var paramsUsedInBehaviour = new HashSet<string>();
foreach (var (behaviourName, behaviourParams) in pmd.Behaviours)
{
foreach (var (behaviourName, behaviourParams) in pmd.Behaviours) {
var sum = 0.0;
var explanation = "";
paramsUsedInBehaviour.UnionWith(behaviourParams.Keys.Select(k => k.Trim('#')));
foreach (var (paramName, _) in pmd.Priority)
{
if (!pmd.DefaultParameters.ContainsKey(paramName))
{
foreach (var (paramName, _) in pmd.Priority) {
if (!pmd.DefaultParameters.ContainsKey(paramName)) {
throw new ArgumentException(
$"The behaviour {behaviourName} uses a parameter for which no default is set: {paramName}");
}
if (!behaviourParams.TryGetValue(paramName, out var weight))
{
if (!behaviourParams.TryGetValue(paramName, out var weight)) {
explanation += $"\n - {paramName} = default (not set)";
continue;
}
var weightObj = weight.Evaluate(context);
if (!(weightObj is double d))
{
if (!(weightObj is double d)) {
throw new ArgumentException(
$"The parameter {paramName} is not a numeric value in profile {behaviourName}");
}
@ -270,8 +241,7 @@ namespace AspectedRouting.Language
explanation += $"\n - {paramName} = {d}";
}
if (Math.Abs(sum) < 0.0001)
{
if (Math.Abs(sum) < 0.0001) {
throw new ArgumentException("Profile " + behaviourName +
": the summed parameters to calculate the weight are zero or very low:" +
explanation);
@ -280,8 +250,7 @@ namespace AspectedRouting.Language
var defaultOnly = defaultParameters.Except(paramsUsedInBehaviour).ToList();
if (defaultOnly.Any())
{
if (defaultOnly.Any()) {
Console.WriteLine(
$"[{pmd.Name}] WARNING: Some parameters only have a default value: {string.Join(", ", defaultOnly)}");
}
@ -289,39 +258,32 @@ namespace AspectedRouting.Language
public static void SanityCheck(this IExpression e)
{
e.Visit(expr =>
{
e.Visit(expr => {
var order = new List<IExpression>();
var mapping = new List<IExpression>();
if (Deconstruct.UnApply(
Deconstruct.UnApply(Deconstruct.IsFunc(Funcs.FirstOf), Deconstruct.Assign(order)),
Deconstruct.Assign(mapping)
).Invoke(expr))
{
var expectedKeys = ((IEnumerable<object>) order.First().Evaluate(null)).Select(o =>
{
if (o is IExpression x)
{
return (string) x.Evaluate(null);
Deconstruct.UnApply(Deconstruct.IsFunc(Funcs.FirstOf), Deconstruct.Assign(order)),
Deconstruct.Assign(mapping)
).Invoke(expr)) {
var expectedKeys = ((IEnumerable<object>)order.First().Evaluate(null)).Select(o => {
if (o is IExpression x) {
return (string)x.Evaluate(null);
}
return (string) o;
return (string)o;
})
.ToHashSet();
var actualKeys = mapping.First().PossibleTags().Keys;
var missingInOrder = actualKeys.Where(key => !expectedKeys.Contains(key)).ToList();
var missingInMapping = expectedKeys.Where(key => !actualKeys.Contains(key)).ToList();
if (missingInOrder.Any() || missingInMapping.Any())
{
if (missingInOrder.Any() || missingInMapping.Any()) {
var missingInOrderMsg = "";
if (missingInOrder.Any())
{
if (missingInOrder.Any()) {
missingInOrderMsg = $"The order misses keys {string.Join(",", missingInOrder)}\n";
}
var missingInMappingMsg = "";
if (missingInMapping.Any())
{
if (missingInMapping.Any()) {
missingInMappingMsg =
$"The mapping misses mappings for keys {string.Join(", ", missingInMapping)}\n";
}
@ -347,49 +309,95 @@ namespace AspectedRouting.Language
public static Dictionary<string, HashSet<string>> PossibleTags(this IEnumerable<IExpression> exprs)
{
var usedTags = new Dictionary<string, HashSet<string>>();
foreach (var expr in exprs)
{
foreach (var expr in exprs) {
var possible = expr.PossibleTags();
if (possible == null)
{
if (possible == null) {
continue;
}
foreach (var (key, values) in possible)
{
if (!usedTags.TryGetValue(key, out var collection))
{
foreach (var (key, values) in possible) {
if (!usedTags.TryGetValue(key, out var collection)) {
// This is the first time we see this collection
collection = new HashSet<string>();
usedTags[key] = collection;
}
foreach (var v in values)
{
foreach (var v in values) {
collection.Add(v);
}
if (values.Count == 0) {
collection.Add("*");
}
}
}
return usedTags;
}
public static Dictionary<string, HashSet<string>> PossibleTagsRecursive(this IEnumerable<IExpression> exprs, Context c)
{ var usedTags = new Dictionary<string, HashSet<string>>();
foreach (var e in exprs) {
var possibleTags = e.PossibleTagsRecursive(c);
if (possibleTags != null) {
foreach (var tag in possibleTags) {
usedTags[tag.Key] = tag.Value;
}
}
}
return usedTags;
}
public static Dictionary<string, HashSet<string>> PossibleTagsRecursive(this IExpression e, Context c)
{
var allExpr = new List<IExpression>();
var queue = new Queue<IExpression>();
queue.Enqueue(e);
do {
var next = queue.Dequeue();
allExpr.Add(next);
next.Visit(expression => {
if (expression is FunctionCall fc) {
var called = c.GetFunction(fc.CalledFunctionName);
queue.Enqueue(called);
}
return true;
});
} while (queue.Any());
var result = new Dictionary<string, HashSet<string>>();
foreach (var expression in allExpr) {
var subTags = expression.PossibleTags();
if (subTags == null) {
continue;
}
foreach (var kv in subTags) {
if (!result.ContainsKey(kv.Key)) {
result[kv.Key] = new HashSet<string>();
}
foreach (var val in kv.Value) {
result[kv.Key].Add(val);
}
}
}
return result;
}
/// <summary>
/// Returns which tags are used in this calculation
///
/// Returns which tags are used in this calculation
/// </summary>
/// <param name="e"></param>
/// <returns>A dictionary containing all possible values. An entry with an empty list indicates a wildcard</returns>
public static Dictionary<string, HashSet<string>> PossibleTags(this IExpression e)
{
var mappings = new List<Mapping>();
e.Visit(x =>
{
e.Visit(x => {
/*
var networkMapping = new List<IExpression>();
if (Deconstruct.UnApply(
@ -402,16 +410,14 @@ namespace AspectedRouting.Language
return false;
}*/
if (x is Mapping m)
{
if (x is Mapping m) {
mappings.Add(m);
}
return true;
});
if (mappings.Count == 0)
{
if (mappings.Count == 0) {
return null;
}
@ -419,13 +425,10 @@ namespace AspectedRouting.Language
var rootMapping = mappings[0];
var result = new Dictionary<string, HashSet<string>>();
foreach (var (key, expr) in rootMapping.StringToResultFunctions)
{
foreach (var (key, expr) in rootMapping.StringToResultFunctions) {
var values = new List<string>();
expr.Visit(x =>
{
if (x is Mapping m)
{
expr.Visit(x => {
if (x is Mapping m) {
values.AddRange(m.StringToResultFunctions.Keys);
}
@ -451,19 +454,16 @@ namespace AspectedRouting.Language
void HandleExpression(IExpression e, string calledIn)
{
e.Visit(f =>
{
e.Visit(f => {
var mapping = new List<IExpression>();
if (Deconstruct.UnApply(Deconstruct.IsFunc(Funcs.MemberOf),
Deconstruct.Assign(mapping)
).Invoke(f))
{
Deconstruct.Assign(mapping)
).Invoke(f)) {
memberships.Add(calledIn, mapping.First());
return false;
}
if (f is FunctionCall fc)
{
if (f is FunctionCall fc) {
calledFunctionQueue.Enqueue(fc.CalledFunctionName);
}
@ -471,15 +471,12 @@ namespace AspectedRouting.Language
});
}
foreach (var e in calledFunctions)
{
foreach (var e in calledFunctions) {
HandleExpression(e, "profile_root");
}
while (calledFunctionQueue.TryDequeue(out var functionName))
{
if (alreadyAnalysedFunctions.Contains(functionName))
{
while (calledFunctionQueue.TryDequeue(out var functionName)) {
if (alreadyAnalysedFunctions.Contains(functionName)) {
continue;
}

View file

@ -96,14 +96,8 @@ namespace AspectedRouting.Language.Expression
}
public ProfileResult Run(Context c, string behaviour, Dictionary<string, string> tags)
public Dictionary<string, IExpression> ParametersFor(string behaviour)
{
if (!Behaviours.ContainsKey(behaviour))
{
throw new ArgumentException(
$"Profile {Name} does not contain the behaviour {behaviour}\nTry one of {string.Join(",", Behaviours.Keys)}");
}
var parameters = new Dictionary<string, IExpression>();
foreach (var (k, v) in DefaultParameters)
@ -116,7 +110,18 @@ namespace AspectedRouting.Language.Expression
parameters[k.TrimStart('#')] = v;
}
c = c.WithParameters(parameters)
return parameters;
}
public ProfileResult Run(Context c, string behaviour, Dictionary<string, string> tags)
{
if (!Behaviours.ContainsKey(behaviour))
{
throw new ArgumentException(
$"Profile {Name} does not contain the behaviour {behaviour}\nTry one of {string.Join(",", Behaviours.Keys)}");
}
c = c.WithParameters(ParametersFor(behaviour))
.WithAspectName(this.Name);
tags = new Dictionary<string, string>(tags);
var canAccess = Access.Run(c, tags);

View file

@ -6,6 +6,7 @@ using AspectedRouting.IO;
using AspectedRouting.IO.itinero1;
using AspectedRouting.IO.itinero2;
using AspectedRouting.IO.jsonParser;
using AspectedRouting.IO.md;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Tests;
@ -18,19 +19,21 @@ namespace AspectedRouting
this IEnumerable<string> jsonFileNames, List<string> testFileNames, Context context)
{
var aspects = new List<(AspectMetadata aspect, AspectTestSuite tests)>();
foreach (var file in jsonFileNames)
{
foreach (var file in jsonFileNames) {
var fi = new FileInfo(file);
var aspect = JsonParser.AspectFromJson(context, File.ReadAllText(file), fi.Name);
if (aspect == null) continue;
if (aspect == null) {
continue;
}
var testName = aspect.Name + ".test.csv";
var testPath = testFileNames.FindTest(testName);
AspectTestSuite tests = null;
if (!string.IsNullOrEmpty(testPath) && File.Exists(testPath))
if (!string.IsNullOrEmpty(testPath) && File.Exists(testPath)) {
tests = AspectTestSuite.FromString(aspect, File.ReadAllText(testPath));
}
aspects.Add((aspect, tests));
}
@ -41,10 +44,13 @@ namespace AspectedRouting
private static string FindTest(this IEnumerable<string> testFileNames, string testName)
{
var testPaths = testFileNames.Where(nm => nm.EndsWith(testName)).ToList();
if (testPaths.Count > 1)
if (testPaths.Count > 1) {
Console.WriteLine("[WARNING] Multiple tests found for " + testName + ", using only one arbitrarily");
}
if (testPaths.Count > 0) return testPaths.First();
if (testPaths.Count > 0) {
return testPaths.First();
}
return null;
}
@ -54,39 +60,37 @@ namespace AspectedRouting
IEnumerable<string> jsonFiles, IReadOnlyCollection<string> testFiles, Context context, DateTime lastChange)
{
var result = new List<(ProfileMetaData profile, List<BehaviourTestSuite> profileTests)>();
foreach (var jsonFile in jsonFiles)
try
{
foreach (var jsonFile in jsonFiles) {
try {
var profile =
JsonParser.ProfileFromJson(context, File.ReadAllText(jsonFile), new FileInfo(jsonFile),
lastChange);
if (profile == null) continue;
if (profile == null) {
continue;
}
profile.SanityCheckProfile(context);
var profileTests = new List<BehaviourTestSuite>();
foreach (var behaviourName in profile.Behaviours.Keys)
{
foreach (var behaviourName in profile.Behaviours.Keys) {
var path = testFiles.FindTest($"{profile.Name}.{behaviourName}.behaviour_test.csv");
if (path != null && File.Exists(path))
{
if (path != null && File.Exists(path)) {
var test = BehaviourTestSuite.FromString(context, profile, behaviourName,
File.ReadAllText(path));
profileTests.Add(test);
}
else
{
else {
Console.WriteLine($"[{profile.Name}] WARNING: no test found for behaviour {behaviourName}");
}
}
result.Add((profile, profileTests));
}
catch (Exception e)
{
catch (Exception e) {
// PrintError(jsonFile, e);
throw new Exception("In the file " + jsonFile, e);
}
}
return result;
}
@ -95,43 +99,44 @@ namespace AspectedRouting
{
var profile = profiles["emergency_vehicle"];
var behaviour = profile.Behaviours.Keys.First();
do
{
do {
Console.Write(profile.Name + "." + behaviour + " > ");
var read = Console.ReadLine();
if (read == null) return; // End of stream has been reached
if (read == null) {
return; // End of stream has been reached
}
if (read == "")
{
if (read == "") {
Console.WriteLine("looƆ sᴉ dɐWʇǝǝɹʇSuǝdO");
continue;
}
if (read.Equals("quit")) return;
if (read.Equals("quit")) {
return;
}
if (read.Equals("help")) {
Console.WriteLine(
Utils.Lines("select <behaviourName> to change behaviour or <vehicle.behaviourName> to change vehicle",
Utils.Lines(
"select <behaviourName> to change behaviour or <vehicle.behaviourName> to change vehicle",
""));
continue;
}
if (read.Equals("clear"))
{
for (var i = 0; i < 80; i++) Console.WriteLine();
if (read.Equals("clear")) {
for (var i = 0; i < 80; i++) {
Console.WriteLine();
}
continue;
}
if (read.StartsWith("select"))
{
if (read.StartsWith("select")) {
var beh = read.Substring("select".Length + 1).Trim();
if (beh.Contains("."))
{
if (beh.Contains(".")) {
var profileName = beh.Split(".")[0];
if (!profiles.TryGetValue(profileName, out var newProfile))
{
if (!profiles.TryGetValue(profileName, out var newProfile)) {
Console.Error.WriteLine("Profile " + profileName + " not found, ignoring");
continue;
}
@ -140,13 +145,11 @@ namespace AspectedRouting
beh = beh.Substring(beh.IndexOf(".") + 1);
}
if (profile.Behaviours.ContainsKey(beh))
{
if (profile.Behaviours.ContainsKey(beh)) {
behaviour = beh;
Console.WriteLine("Switched to " + beh);
}
else
{
else {
Console.WriteLine("Behaviour not found. Known behaviours are:\n " +
string.Join("\n ", profile.Behaviours.Keys));
}
@ -157,9 +160,10 @@ namespace AspectedRouting
var tagsRaw = read.Split(";").Select(s => s.Trim());
var tags = new Dictionary<string, string>();
foreach (var str in tagsRaw)
{
if (str == "") continue;
foreach (var str in tagsRaw) {
if (str == "") {
continue;
}
var strSplit = str.Split("=");
var k = strSplit[0].Trim();
@ -167,13 +171,11 @@ namespace AspectedRouting
tags[k] = v;
}
try
{
try {
var result = profile.Run(c, behaviour, tags);
Console.WriteLine(result);
}
catch (Exception e)
{
catch (Exception e) {
Console.WriteLine(e);
Console.WriteLine(e.Message);
}
@ -183,8 +185,7 @@ namespace AspectedRouting
private static void PrintError(string file, Exception exception)
{
var msg = exception.Message;
while (exception.InnerException != null)
{
while (exception.InnerException != null) {
exception = exception.InnerException;
msg += "\n " + exception.Message;
}
@ -195,10 +196,11 @@ namespace AspectedRouting
private static void PrintUsedTags(ProfileMetaData profile, Context context)
{
Console.WriteLine("\n\n\n---------- " + profile.Name + " --------------");
foreach (var (key, values) in profile.AllExpressions(context).PossibleTags())
{
foreach (var (key, values) in profile.AllExpressions(context).PossibleTags()) {
var vs = "*";
if (values.Any()) vs = string.Join(", ", values);
if (values.Any()) {
vs = string.Join(", ", values);
}
Console.WriteLine(key + ": " + vs);
}
@ -209,18 +211,23 @@ namespace AspectedRouting
private static void Main(string[] args)
{
var errMessage = MainWithError(args);
if (errMessage != null) Console.WriteLine(errMessage);
if (errMessage != null) {
Console.WriteLine(errMessage);
}
}
public static string MainWithError(string[] args)
{
if (args.Length < 2)
if (args.Length < 2) {
return "Usage: <directory where all aspects and profiles can be found> <outputdirectory>";
}
var inputDir = args[0];
var outputDir = args[1];
if (!Directory.Exists(outputDir)) Directory.CreateDirectory(outputDir);
if (!Directory.Exists(outputDir)) {
Directory.CreateDirectory(outputDir);
}
MdPrinter.GenerateHelpText(outputDir + "helpText.md");
@ -232,9 +239,10 @@ namespace AspectedRouting
.ToList();
tests.Sort();
foreach (var test in tests)
{
if (test.EndsWith(".test.csv") || test.EndsWith(".behaviour_test.csv")) continue;
foreach (var test in tests) {
if (test.EndsWith(".test.csv") || test.EndsWith(".behaviour_test.csv")) {
continue;
}
throw new ArgumentException(
$"Invalid name for csv file ${test}, should end with either '.behaviour_test.csv' or '.test.csv'");
@ -244,14 +252,14 @@ namespace AspectedRouting
var aspects = ParseAspects(files, tests, context);
foreach (var (aspect, _) in aspects) context.AddFunction(aspect.Name, aspect);
foreach (var (aspect, _) in aspects) {
context.AddFunction(aspect.Name, aspect);
}
var lastChange = DateTime.UnixEpoch;
foreach (var file in files)
{
foreach (var file in files) {
var time = new FileInfo(file).LastWriteTimeUtc;
if (lastChange < time)
{
if (lastChange < time) {
lastChange = time;
}
}
@ -261,21 +269,34 @@ namespace AspectedRouting
// With everything parsed and typechecked, time for tests
var testsOk = true;
foreach (var (aspect, t) in aspects)
if (t == null)
foreach (var (aspect, t) in aspects) {
if (t == null) {
Console.WriteLine($"[{aspect.Name}] WARNING: no tests found: please add {aspect.Name}.test.csv");
else
}
else {
testsOk &= t.Run();
}
}
foreach (var (profile, profileTests) in profiles)
foreach (var test in profileTests)
foreach (var test in profileTests) {
testsOk &= test.Run(context);
}
if (!testsOk) return "Some tests failed, quitting now without generating output";
if (!testsOk) {
return "Some tests failed, quitting now without generating output";
}
foreach (var (profile, profileTests) in profiles)
{
if (!Directory.Exists($"{outputDir}/profile-documentation/")) {
Directory.CreateDirectory($"{outputDir}/profile-documentation/");
}
if (!Directory.Exists($"{outputDir}/itinero2/")) {
Directory.CreateDirectory($"{outputDir}/itinero2/");
}
foreach (var (profile, profileTests) in profiles) {
PrintUsedTags(profile, context);
var aspectTests = aspects.Select(a => a.tests).ToList();
@ -285,8 +306,23 @@ namespace AspectedRouting
).ToLua();
File.WriteAllText(outputDir + "/" + profile.Name + ".lua", luaProfile);
foreach (var (behaviourName, _) in profile.Behaviours)
{
var profileMd = new MarkDownSection();
profileMd.AddTitle(profile.Name, 1);
profileMd.Add(profile.Description);
profileMd.AddTitle("Default parameters", 4);
profileMd.Add("| name | value | ", "| ---- | ---- | ",
string.Join("\n",
profile.DefaultParameters.Select(delegate(KeyValuePair<string, IExpression> kv) {
var v = kv.Value.Evaluate(context);
if (!(v is string || v is int || v is double)) {
v = "_special value_";
}
return $" | {kv.Key} | {v} |";
}))
);
foreach (var (behaviourName, vars) in profile.Behaviours) {
var lua2behaviour = new LuaPrinter2(
profile,
behaviourName,
@ -295,12 +331,24 @@ namespace AspectedRouting
profileTests.Where(testsSuite => testsSuite.BehaviourName == behaviourName),
lastChange
).ToLua();
if (!Directory.Exists($"{outputDir}/itinero2/"))
Directory.CreateDirectory($"{outputDir}/itinero2/");
File.WriteAllText(
$"{outputDir}/itinero2/{profile.Name}.{behaviourName}.lua",
lua2behaviour);
var behaviourMd = new ProfileToMD(profile, behaviourName, context);
File.WriteAllText(
$"{outputDir}/profile-documentation/{profile.Name}.{behaviourName}.md",
behaviourMd.ToString());
profileMd.AddTitle($"[{behaviourName}](./{behaviourName}.md)", 2);
profileMd.Add(vars["description"].Evaluate(context).ToString());
profileMd.Add(behaviourMd.MainFormula());
}
File.WriteAllText(
$"{outputDir}/profile-documentation/{profile.Name}.md",
profileMd.ToString());
}
File.WriteAllText($"{outputDir}/ProfileMetadata.json",
@ -310,12 +358,16 @@ namespace AspectedRouting
Utils.GenerateTagsOverview(profiles.Select(p => p.profile), context)
);
if (!args.Contains("--no-repl"))
if (!args.Contains("--no-repl")) {
Repl(context, profiles
.Select(p => p.profile)
.ToDictionary(p => p.Name, p => p));
else
}
else {
Console.WriteLine("Not starting REPL as --no-repl is specified");
}
return null;
}
}

View file

@ -42,5 +42,13 @@ namespace AspectedRouting.Tests
"because \n "+str(PriorityExplanation)
);
}
public override bool Equals(object? obj)
{
if (!(obj is ProfileResult other)) {
return false;
}
return other.Access == this.Access && other.Oneway == this.Oneway && other.Priority == this.Priority && other.Speed == this.Speed;
}
}
}