Nearly working version

This commit is contained in:
Pieter Vander Vennet 2020-05-02 13:09:49 +02:00
parent 2c2a28d30a
commit 62584c9189
94 changed files with 4011 additions and 4143 deletions

View file

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</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" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AspectedRouting\AspectedRouting.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,63 @@
using System.Collections.Generic;
using AspectedRouting.IO.jsonParser;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using Xunit;
namespace AspectedRouting.Test
{
public class FunctionsTest
{
private IExpression MustMatchJson()
{
var json = "{" +
"\"name\":\"test\"," +
"\"description\":\"test\"," +
"\"$mustMatch\":{\"a\":\"b\",\"x\":\"y\"}}";
return JsonParser.AspectFromJson(new Context(), json, "test.json");
}
[Fact]
public void TestAll_AllTags_Yes()
{
var tagsAx = new Dictionary<string, string>
{
{"a", "b"},
{"x", "y"}
};
var expr = new Apply(MustMatchJson(), new Constant(tagsAx)).Optimize();
var result = expr.Evaluate(new Context());
Assert.Equal("yes", result);
}
[Fact]
public void TestAll_NoMatch_No()
{
var tagsAx = new Dictionary<string, string>
{
{"a", "b"},
};
var expr = new Apply(MustMatchJson(), new Constant(tagsAx)).Optimize();
var result = expr.Evaluate(new Context());
Assert.Equal("no", result);
}
[Fact]
public void TestAll_NoMatchDifferent_No()
{
var tagsAx = new Dictionary<string, string>
{
{"a", "b"},
{"x", "someRandomValue"}
};
var expr = new Apply(MustMatchJson(), new Constant(tagsAx)).Optimize();
var result = expr.Evaluate(new Context());
Assert.Equal("no", result);
}
}
}

View file

@ -0,0 +1,84 @@
using System.Collections.Generic;
using AspectedRouting.IO.itinero1;
using AspectedRouting.Language;
using AspectedRouting.Language.Functions;
using Xunit;
namespace AspectedRouting.Test
{
public class LuaPrinterTest
{
[Fact]
public void ToLua_SimpleMapping_Table()
{
var mapping = new Mapping(
new[] {"a", "b", "c"},
new[]
{
new Constant(5),
new Constant(6),
new Constant(7),
}
);
var luaPrinter = new LuaPrinter(new Context());
var result = luaPrinter.MappingToLua(mapping);
Assert.Equal(
"{\n a = 5,\n b = 6,\n c = 7\n}"
, result);
}
[Fact]
public void ToLua_NestedMapping_Table()
{
var mapping = new Mapping(
new[] {"a"},
new[]
{
new Mapping(new[] {"b"},
new[]
{
new Constant(42),
}
)
}
);
var luaPrinter = new LuaPrinter(new Context());
var result = luaPrinter.MappingToLua(mapping);
Assert.Equal("{\n a = {\n b = 42\n }\n}", result);
}
[Fact]
public void Sanity_EveryBasicFunction_HasDescription()
{
var missing = new List<string>();
foreach (var (_, f) in Funcs.Builtins)
{
if (string.IsNullOrEmpty(f.Description))
{
missing.Add(f.Name);
}
}
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)
{
missing.Add(f.Name);
}
}
Assert.True(0 == missing.Count,
"These functions do not have a description: " + string.Join(", ", missing));
}
}
}

View file

@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Language;
using AspectedRouting.Language.Functions;
using Xunit;
namespace AspectedRouting.Test
{
public class TestAnalysis
{
[Fact]
public void OnAll_SmallTagSet_AllCombinations()
{
var possibleTags = new Dictionary<string, List<string>>
{
{"a", new List<string> {"x", "y"}}
};
var all = possibleTags.OnAllCombinations(dict => ObjectExtensions.Pretty(dict), new List<string>()).ToList();
Assert.Equal(3, all.Count);
Assert.Contains("{}", all);
Assert.Contains("{a=x;}", all);
Assert.Contains("{a=y;}", all);
}
[Fact]
public void OnAll_TwoTagSet_AllCombinations()
{
var possibleTags = new Dictionary<string, List<string>>
{
{"a", new List<string> {"x", "y"}},
{"b", new List<string> {"u", "v"}}
};
var all = possibleTags.OnAllCombinations(dict => dict.Pretty(), new List<string>()).ToList();
Assert.Equal(9, all.Count);
Assert.Contains("{}", all);
Assert.Contains("{a=x;}", all);
Assert.Contains("{a=y;}", all);
Assert.Contains("{b=u;}", all);
Assert.Contains("{b=v;}", all);
Assert.Contains("{a=x;b=u;}", all);
Assert.Contains("{a=x;b=v;}", all);
Assert.Contains("{a=y;b=u;}", all);
Assert.Contains("{a=y;b=v;}", all);
}
}
}

View file

@ -0,0 +1,182 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.IO.jsonParser;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
using Xunit;
namespace AspectedRouting.Test
{
public class TestInterpreter
{
[Fact]
public void MaxSpeedAspect_Evaluate_CorrectMaxSpeed()
{
var json =
"{\"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"}
};
Assert.Equal("tags -> double", string.Join(", ", aspect.Types));
Assert.Equal(42d, new Apply(aspect, new Constant(tags)).Evaluate(null));
}
[Fact]
public void MaxSpeed_AnalyzeTags_AllTagsReturned()
{
var json =
"{\"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);
Assert.Equal(
new Dictionary<string, List<string>>
{
{"maxspeed", new List<string>()},
{"highway", new List<string> {"residential"}},
{"ferry", new List<string>()}
},
aspect.PossibleTags());
}
[Fact]
public void EitherFunc_Const_Id_Specialized_Has_Correct_Type()
{
var b = new Var("b");
var c = new Var("c");
var eitherFunc = new Apply(Funcs.Const, Funcs.Id);
Assert.Single(eitherFunc.Types);
Assert.Equal(new Curry(b,
new Curry(c, c)), eitherFunc.Types.First());
var (_, (func, arg)) = eitherFunc.FunctionApplications.First();
// func == const : (c -> c) -> b -> (c -> c)
var funcType = new Curry(new Curry(c, c),
new Curry(b, new Curry(c, c)));
Assert.Equal(funcType, func.Types.First());
// arg == id (but specialized): (c -> c)
var argType = new Curry(c, c);
Assert.Equal(argType, arg.Types.First());
}
[Fact]
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);
Assert.Equal("((($const $id) $const) \"a\")", specialized.ToString());
Assert.Equal("string; $b -> string", string.Join("; ", new Apply(mconst, a).Types));
Assert.Equal("\"a\"", specialized.Evaluate(null).Pretty());
Assert.Equal("\"a\"", new Apply(new Apply(mconst, a), new Constant("42")).Specialize(Typs.String)
.Evaluate(null)
.Pretty());
}
[Fact]
public void Parse_Five_5()
{
var str = new Constant("5");
var parsed = new Apply(Funcs.Parse, str).Specialize(Typs.Double);
var o = parsed.Evaluate(null);
Assert.Equal(5d, o);
}
[Fact]
public void Concat_TwoString_AB()
{
var a = new Constant("a");
var b = new Constant("b");
Assert.Equal("string -> string -> string", Funcs.Concat.Types.First().ToString());
var ab = Funcs.Concat.Apply(a, b);
Assert.Equal("(($concat \"a\") \"b\")", ab.ToString());
Assert.Equal("ab", ab.Evaluate(null));
}
[Fact]
public void Id_Evaluate_ReturnsInput()
{
var a = new Constant("a");
var aId = Funcs.Id.Apply(a);
Assert.Equal("\"a\" : string", aId + " : " + string.Join(", ", aId.Types));
Assert.Equal("a", aId.Evaluate(null));
}
[Fact]
public void MaxTest()
{
var ls = new Constant(new ListType(Typs.Double),
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);
Assert.Equal("($max [1.1, 2, 3]) : double", mx + " : " + string.Join(", ", mx.Types));
Assert.Equal(3d, mx.Evaluate(null));
var mxId = Funcs.Id.Apply(mx);
// identity function is not printed
Assert.Equal("($max [1.1, 2, 3]) : double", mxId + " : " + string.Join(", ", mxId.Types));
Assert.Equal(3d, mxId.Evaluate(null));
}
[Fact]
public void Fresh_SomeType_NewVarName()
{
Assert.Equal("$d", Var.Fresh(
Curry.ConstructFrom(new Var("a"),
new Var("b"),
new Var("b"),
new Var("c"),
new Var("aa"))).Name);
}
[Fact]
public void Mapping_Evaluate_CorrectMapping()
{
var mapping = Mapping.Construct(
("a", new Constant(5.0)),
("b", new Constant(-3))
);
Assert.Equal("5", mapping.Evaluate(null, new Constant("a")).Pretty());
Assert.Equal("-3", mapping.Evaluate(null, new Constant("b")).Pretty());
}
[Fact]
public void TestStringGeneration()
{
var v = Var.Fresh(new HashSet<string> {"$a", "$b"});
Assert.Equal("$c", v.Name);
}
[Fact]
public void TestDeconstruct()
{
var a = new Constant("a");
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()),
args.Select(e => e.ToString()));
}
}
}

22
AspectedRouting.sln Normal file
View file

@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspectedRouting", "AspectedRouting\AspectedRouting.csproj", "{4A53D888-6872-4E5D-8022-338CC302473F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspectedRouting.Test", "AspectedRouting.Test\AspectedRouting.Test.csproj", "{A1309041-8AAE-42D7-A886-94C9FFC6A28C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4A53D888-6872-4E5D-8022-338CC302473F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A53D888-6872-4E5D-8022-338CC302473F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A53D888-6872-4E5D-8022-338CC302473F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A53D888-6872-4E5D-8022-338CC302473F}.Release|Any CPU.Build.0 = Release|Any CPU
{A1309041-8AAE-42D7-A886-94C9FFC6A28C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1309041-8AAE-42D7-A886-94C9FFC6A28C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1309041-8AAE-42D7-A886-94C9FFC6A28C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1309041-8AAE-42D7-A886-94C9FFC6A28C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -1,99 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
namespace AspectedRouting.Functions
{
public class MemberOf : Function
{
public override string Description { get; } =
"This function uses memberships of relations to calculate values.\n" +
"\n" +
"Consider all the relations the scrutinized way is part of." +
"The enclosed function is executed for every single relation which is part of it, generating a list of results." +
"This list of results is in turn returned by 'memberOf'" +
"\n" +
"In itinero 1/lua, this is implemented by converting the matching relations and by adding the tags of the relations to the dictionary (or table) with the highway tags." +
"The prefix is '_relation:n:key=value', where 'n' is a value between 0 and the number of matching relations (implying that all of these numbers are scanned)." +
"The matching relations can be extracted by the compiler for the preprocessing.\n\n" +
"For testing, the relation can be emulated by using e.g. '_relation:0:key=value'";
public override List<string> ArgNames { get; } = new List<string>
{
"f","tags"
};
public MemberOf() : base(
"memberOf", true,
new[]
{
new Curry(
new Curry(Typs.Tags, new Var("a")),
new Curry(Typs.Tags, new ListType(new Var("a"))))
}
)
{
}
public MemberOf(IEnumerable<Type> types) : base("memberOf", types)
{
}
public override object Evaluate(Context c, params IExpression[] arguments)
{
var f = arguments[0];
var tags = (Dictionary<string, string>) arguments[1].Evaluate(c);
var prefixes = new Dictionary<int, Dictionary<string, string>>();
foreach (var (k, v) in tags)
{
if (!k.StartsWith("_relation:")) continue;
var s = k.Split(":")[1];
if (int.TryParse(s, out var i))
{
var key = k.Substring(("_relation:" + i + ":").Length);
if (prefixes.TryGetValue(i, out var relationTags))
{
relationTags[key] = v;
}
else
{
prefixes[i] = new Dictionary<string, string>
{
{key, v}
};
}
}
}
// At this point, we have all the tags of all the relations
// Time to run the function on all of them
var result = new List<object>();
foreach (var relationTags in prefixes.Values)
{
var o = f.Evaluate(c, new Constant(relationTags));
if (o != null)
{
result.Add(o);
}
}
return result;
}
public override IExpression Specialize(IEnumerable<Type> allowedTypes)
{
var unified = Types.SpecializeTo(allowedTypes);
if (unified == null)
{
return null;
}
return new MemberOf(unified);
}
}
}

View file

@ -1,42 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Functions;
namespace AspectedRouting.IO
{
public static class JsonPrinter
{
private static string Objs(params (string k, string v)[] stuff)
{
return "{\n" + string.Join("\n", stuff.Where(kv => kv.v != null).Select(kv => kv.k.Dq() + ": " + kv.v)).Indent() + "\n}";
}
private static string Arr(IEnumerable<string> strs)
{
return "[" + string.Join(", ", strs) + "]";
}
private static string Dq(this string s)
{
if (s == null)
{
return null;
}
return '"' + s + '"';
}
public static string Print(AspectMetadata meta)
{
return Objs(
("name", meta.Name.Dq()),
("description", meta.Description.Dq()),
("unit", meta.Unit.Dq()),
("author", meta.Author.Dq()),
("file", meta.Filepath.Dq()),
("type", Arr(meta.Types.Select(tp => tp.ToString().Dq()))),
("value", meta.ExpressionImplementation.ToString())
);
}
}
}

View file

@ -1,535 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using AspectedRouting.Functions;
using AspectedRouting.Typ;
using static AspectedRouting.Deconstruct;
namespace AspectedRouting.IO
{
public class LuaPrinter
{
public static Dictionary<string, string> BasicFunctions = _basicFunctions();
private readonly HashSet<string> _deps = new HashSet<string>();
private readonly List<string> _code = new List<string>();
private readonly HashSet<string> _neededKeys = new HashSet<string>();
/// <summary>
/// A dictionary containing the implementation of basic functions
/// </summary>
/// <returns></returns>
private static Dictionary<string, string> _basicFunctions()
{
var imps = new Dictionary<string, string>();
var functionsToFetch = Funcs.BuiltinNames.ToList();
// These functions should be loaded from disk, but are not necessarily included
functionsToFetch.Add("table_to_list");
functionsToFetch.Add("debug_table");
functionsToFetch.Add("unitTest");
functionsToFetch.Add("unitTestProfile");
functionsToFetch.Add("double_compare");
foreach (var name in functionsToFetch)
{
var path = $"IO/lua/{name}.lua";
if (File.Exists(path))
{
imps[name] = File.ReadAllText(path);
}
}
return imps;
}
public LuaPrinter()
{
_deps.Add("debug_table");
}
private string ToLua(IExpression bare, Context context, string key = "nil")
{
var collectedMapping = new List<IExpression>();
var order = new List<IExpression>();
if (UnApply(
UnApply(
IsFunc(Funcs.FirstOf),
Assign(order))
, UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))
).Invoke(bare))
{
_deps.Add(Funcs.FirstOf.Name);
return "first_match_of(tags, result, \n" +
" " + ToLua(order.First(), context, key) + "," +
("\n" + MappingToLua((Mapping) collectedMapping.First(), context)).Indent().Indent() +
")";
}
if (UnApply(
UnApply(
IsFunc(Funcs.MustMatch),
Assign(order))
, UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))
).Invoke(bare))
{
_deps.Add(Funcs.MustMatch.Name);
return "must_match(tags, result, \n" +
" " + ToLua(order.First(), context, key) + "," +
("\n" + MappingToLua((Mapping) collectedMapping.First(), context)).Indent().Indent() +
")";
}
var collectedList = new List<IExpression>();
var func = new List<IExpression>();
if (
UnApply(
UnApply(IsFunc(Funcs.Dot), Assign(func)),
UnApply(IsFunc(Funcs.ListDot),
Assign(collectedList))).Invoke(bare))
{
var exprs = (IEnumerable<IExpression>) ((Constant) collectedList.First()).Evaluate(context);
var luaExprs = new List<string>();
var funcName = func.First().ToString().TrimStart('$');
_deps.Add(funcName);
foreach (var expr in exprs)
{
var c = new List<IExpression>();
if (UnApply(IsFunc(Funcs.Const), Assign(c)).Invoke(expr))
{
luaExprs.Add(ToLua(c.First(), context, key));
continue;
}
if (expr.Types.First() is Curry curry
&& curry.ArgType.Equals(Typs.Tags))
{
var lua = ToLua(expr, context, key);
luaExprs.Add(lua);
}
}
return "\n " + funcName + "({\n " + string.Join(",\n ", luaExprs) +
"\n })";
}
collectedMapping.Clear();
var dottedFunction = new List<IExpression>();
dottedFunction.Clear();
if (UnApply(
UnApply(
IsFunc(Funcs.Dot),
Assign(dottedFunction)
),
UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))).Invoke(bare)
)
{
var mapping = (Mapping) collectedMapping.First();
var baseFunc = (Function) dottedFunction.First();
_deps.Add(baseFunc.Name);
_deps.Add("table_to_list");
return baseFunc.Name +
"(table_to_list(tags, result, " +
("\n" + MappingToLua(mapping, context)).Indent().Indent() +
"))";
}
// The expression might be a function which still expects a string (the value from the tag) as argument
if (!(bare is Mapping) &&
bare.Types.First() is Curry curr &&
curr.ArgType.Equals(Typs.String))
{
var applied = new Apply(bare, new Constant(curr.ArgType, ("tags", "\"" + key + "\"")));
return ToLua(applied.Optimize(), context, key);
}
// The expression might consist of multiple nested functions
var fArgs = bare.DeconstructApply();
if (fArgs != null)
{
var (f, args) = fArgs.Value;
var baseFunc = (Function) f;
_deps.Add(baseFunc.Name);
return baseFunc.Name + "(" + string.Join(", ", args.Select(arg => ToLua(arg, context, key))) + ")";
}
var collected = new List<IExpression>();
switch (bare)
{
case FunctionCall fc:
var called = context.DefinedFunctions[fc.CalledFunctionName];
if (called.ProfileInternal)
{
return called.Name;
}
AddFunction(called, context);
return $"{fc.CalledFunctionName.FunctionName()}(parameters, tags, result)";
case Constant c:
return ConstantToLua(c, context);
case Mapping m:
return MappingToLua(m, context).Indent();
case Function f:
var fName = f.Name.TrimStart('$');
if (Funcs.Builtins.ContainsKey(fName))
{
_deps.Add(f.Name);
}
else
{
var definedFunc = context.DefinedFunctions[fName];
if (definedFunc.ProfileInternal)
{
return f.Name;
}
AddFunction(definedFunc, context);
}
return f.Name;
case Apply a when UnApply(IsFunc(Funcs.Const), Assign(collected)).Invoke(a):
return ToLua(collected.First(), context, key);
case Parameter p:
return $"parameters[\"{p.ParamName.FunctionName()}\"]";
default:
throw new Exception("Could not convert " + bare + " to a lua expression");
}
}
public static string ConstantToLua(Constant c, Context context)
{
var o = c.Evaluate(context);
switch (o)
{
case IExpression e:
return ConstantToLua(new Constant(e.Types.First(), e.Evaluate(null)), context);
case int i:
return "" + i;
case double d:
return "" + d;
case string s:
return '"' + s.Replace("\"", "\\\"") + '"';
case ValueTuple<string, string> unpack:
return unpack.Item1 + "[" + unpack.Item2 + "]";
case IEnumerable<object> ls:
var t = (c.Types.First() as ListType).InnerType;
return "{" + string.Join(", ", ls.Select(obj =>
{
var objInConstant = new Constant(t, obj);
if (obj is Constant asConstant)
{
objInConstant = asConstant;
}
return ConstantToLua(objInConstant, context);
})) + "}";
default:
return o.ToString();
}
}
public string MappingToLua(Mapping m, Context context)
{
var contents = m.StringToResultFunctions.Select(kv =>
{
var (key, expr) = kv;
var left = "[\"" + key + "\"]";
if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$"))
{
left = key;
}
return left + " = " + ToLua(expr, context, key);
}
);
return
"{\n " +
string.Join(",\n ", contents) +
"\n}";
}
private HashSet<string> addFunctions = new HashSet<string>();
public void AddFunction(AspectMetadata meta, Context context)
{
if (addFunctions.Contains(meta.Name))
{
// already added
return;
}
addFunctions.Add(meta.Name);
var possibleTags = meta.PossibleTags();
var usedParams = meta.UsedParameters();
var numberOfCombinations = possibleTags.Values.Select(lst => 1 + lst.Count).Multiply();
var impl = string.Join("\n",
"--[[",
meta.Description,
"",
"Unit: " + meta.Unit,
"Created by " + meta.Author,
"Originally defined in " + meta.Filepath,
"Uses tags: " + string.Join(", ", possibleTags.Keys),
"Used parameters: " + string.Join(", ", usedParams),
"Number of combintations: " + numberOfCombinations,
"Returns values: ",
"]]",
"function " + meta.Name.FunctionName() + "(parameters, tags, result)",
" return " + ToLua(meta.ExpressionImplementation, context),
"end"
);
_code.Add(impl);
foreach (var k in possibleTags.Keys)
{
_neededKeys.Add(k); // To generate a whitelist of OSM-keys that should be kept
}
}
private string CreateMembershipPreprocessor()
{
return "";
}
/// <summary>
/// Adds the necessary called functions and the profile main entry point
/// </summary>
public void CreateProfile(ProfileMetaData profile, Context context)
{
var defaultParameters = "\n";
foreach (var (name, (types, inFunction)) in profile.UsedParameters(context))
{
defaultParameters += $"{name}: {string.Join(", ", types)}\n" +
$" Used in {inFunction}\n";
}
var impl = string.Join("\n",
"",
"",
$"name = \"{profile.Name}\"",
"normalize = false",
"vehicle_type = {" + string.Join(", ", profile.VehicleTyps.Select(s => "\"" + s + "\"")) + "}",
"meta_whitelist = {" + string.Join(", ", profile.Metadata.Select(s => "\"" + s + "\"")) + "}",
"",
"",
"",
"--[[",
profile.Name,
"This is the main function called to calculate the access, oneway and speed.",
"Comfort is calculated as well, based on the parameters which are padded in",
"",
"Created by " + profile.Author,
"Originally defined in " + profile.Filename,
"Used parameters: " + defaultParameters.Indent(),
"]]",
"function " + profile.Name + "(parameters, tags, result)",
"",
" -- initialize the result table on the default values",
" result.access = 0",
" result.speed = 0",
" result.factor = 1",
" result.direction = 0",
" result.canstop = true",
" result.attributes_to_keep = {}",
"",
" local access = " + ToLua(profile.Access, context),
" if (access == nil or access == \"no\") then",
" return",
" end",
" local oneway = " + ToLua(profile.Oneway, context),
" local speed = " + ToLua(profile.Speed, context),
" local distance = 1 -- the weight per meter for distance travelled is, well, 1m/m",
"");
impl +=
"\n local weight = \n ";
var weightParts = new List<string>();
foreach (var (parameterName, expression) in profile.Weights)
{
var weightPart = ToLua(new Parameter(parameterName), context) + " * ";
var subs = new Curry(Typs.Tags, new Var(("a"))).UnificationTable(expression.Types.First());
if (subs != null && subs.TryGetValue("$a", out var resultType) &&
(resultType.Equals(Typs.Bool) || resultType.Equals(Typs.String)))
{
weightPart += "parse(" + ToLua(expression, context) + ")";
}
else
{
weightPart += ToLua(expression, context);
}
weightParts.Add(weightPart);
}
impl += string.Join(" + \n ", weightParts);
impl += string.Join("\n",
"",
"",
"",
" -- put all the values into the result-table, as needed for itinero",
" result.access = 1",
" result.speed = speed",
" result.factor = 1/weight",
"",
" if (oneway == \"both\") then",
" result.oneway = 0",
" elseif (oneway == \"with\") then",
" result.oneway = 1",
" else",
" result.oneway = 2",
" end",
"",
"end",
"",
"",
"function default_parameters()",
" return " + profile.DefaultParameters.ToLuaTable(),
"end",
"",
""
);
impl += "\n\n" + CreateMembershipPreprocessor() + "\n\n";
var profiles = new List<string>();
foreach (var (name, subParams) in profile.Profiles)
{
var functionName = profile.Name + "_" + name;
subParams.TryGetValue("description", out var description);
profiles.Add(
string.Join(",\n ",
$" name = \"{name}\"",
" function_name = \"profile_" + functionName + "\"",
" metric = \"custom\""
)
);
impl += string.Join("\n",
"",
"--[[",
description,
"]]",
"function profile_" + functionName + "(tags, result)",
" local parameters = default_parameters()",
""
);
foreach (var (paramName, value) in subParams)
{
impl += $" parameters.{paramName.TrimStart('#').FunctionName()} = {value.Pretty()}\n";
}
impl += " " + profile.Name + "(parameters, tags, result)\n";
impl += "end\n";
}
impl += "\n\n\n";
impl += "profiles = {\n {\n" +
string.Join("\n },\n {\n ", profiles) + "\n }\n}";
_code.Add(impl);
}
public string ToLua()
{
var deps = _deps.ToList();
deps.Add("unitTest");
deps.Add("unitTestProfile");
deps.Add("inv");
deps.Add("double_compare");
deps.Sort();
var code = deps.Select(d => BasicFunctions[d]).ToList();
var keys = _neededKeys.Select(key => "\"" + key + "\"");
code.Add("\n\nprofile_whitelist = {" + string.Join(", ", keys) + "}");
code.AddRange(_code);
return string.Join("\n\n\n", code);
}
}
public static class StringExtensions
{
public static string ToLuaTable(this Dictionary<string, string> tags)
{
var contents = tags.Select(kv =>
{
var (key, value) = kv;
var left = "[\"" + key + "\"]";
if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$"))
{
left = key;
}
return $"{left} = \"{value}\"";
});
return "{" + string.Join(", ", contents) + "}";
}
public static string ToLuaTable(this Dictionary<string, object> tags)
{
var contents = tags.Select(kv =>
{
var (key, value) = kv;
var left = "[\"" + key + "\"]";
if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$"))
{
left = key;
}
return $"{left} = {value.Pretty()}";
});
return "{" + string.Join(", ", contents) + "}";
}
public static string FunctionName(this string s)
{
return s.Replace(" ", "_").Replace(".", "_").Replace("-", "_");
}
}
}

View file

@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AspectedRouting.Functions;
using AspectedRouting.Typ;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.IO
{
@ -32,7 +32,7 @@ namespace AspectedRouting.IO
get
{
var txt = "## Builtin functions\n\n";
foreach (var biFunc in Functions.Funcs.BuiltinNames)
foreach (var biFunc in Funcs.BuiltinNames)
{
txt += "- " + biFunc + "\n";
}
@ -68,7 +68,7 @@ namespace AspectedRouting.IO
{
var args = f.ArgBreakdown();
var header = "Argument name | ";
var line = "--------------| - ";
var line = "-------------- | - ";
for (int i = 0; i < f.Types.Count(); i++)
{
header += "| ";

View file

@ -1,135 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Functions;
namespace AspectedRouting.IO
{
public struct Expected
{
public int Access, Oneway;
public double Speed, Weight;
public Expected(int access, int oneway, double speed, double weight)
{
Access = access;
Oneway = oneway;
Speed = speed;
Weight = weight;
if (Access == 0)
{
}
}
}
public class ProfileTestSuite
{
private readonly ProfileMetaData _profile;
private readonly string _profileName;
private readonly IEnumerable<(Expected, Dictionary<string, string> tags)> _tests;
public static ProfileTestSuite FromString(ProfileMetaData function, string profileName, string csvContents)
{
try
{
var all = csvContents.Split("\n").ToList();
var keys = all[0].Split(",").ToList();
keys = keys.GetRange(4, keys.Count - 4).Select(k => k.Trim()).ToList();
var tests = new List<(Expected, Dictionary<string, string>)>();
var line = 1;
foreach (var test in all.GetRange(1, all.Count - 1))
{
line++;
if (string.IsNullOrEmpty(test.Trim()))
{
continue;
}
try
{
var testData = test.Split(",").ToList();
var expected = new Expected(
int.Parse(testData[0]),
int.Parse(testData[1]),
double.Parse(testData[2]),
double.Parse(testData[3])
);
var vals = testData.GetRange(4, testData.Count - 4);
var tags = new Dictionary<string, string>();
for (int i = 0; i < keys.Count; i++)
{
if (i < vals.Count && !string.IsNullOrEmpty(vals[i]))
{
tags[keys[i]] = vals[i];
}
}
tests.Add((expected, tags));
}
catch (Exception e)
{
throw new Exception("On line " + line, e);
}
}
return new ProfileTestSuite(function, profileName, tests);
}
catch (Exception e)
{
throw new Exception("In the profile test file for " + profileName, e);
}
}
public ProfileTestSuite(
ProfileMetaData profile,
string profileName,
IEnumerable<(Expected, Dictionary<string, string> tags)> tests)
{
_profile = profile;
_profileName = profileName;
_tests = tests;
}
public string ToLua()
{
var tests = string.Join("\n",
_tests.Select((test, i) => ToLua(i, test.Item1, test.tags)));
return tests + "\n";
}
private string ToLua(int index, Expected expected, Dictionary<string, string> tags)
{
var parameters = new Dictionary<string, string>();
foreach (var (key, value) in tags)
{
if (key.StartsWith("#"))
{
parameters[key.TrimStart('#')] = value;
}
}
foreach (var (paramName, _) in parameters)
{
tags.Remove("#" + paramName);
}
// function unit_test_profile(profile_function, profile_name, index, expected, tags)
return $"unit_test_profile(profile_bicycle_{_profileName.FunctionName()}, " +
$"\"{_profileName}\", " +
$"{index}, " +
$"{{access = {expected.Access}, speed = {expected.Speed}, oneway = {expected.Oneway}, weight = {expected.Weight} }}, " +
tags.ToLuaTable() +
")";
}
public void Run()
{
}
}
}

View file

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace AspectedRouting.IO.itinero1
{
public partial class LuaPrinter
{
private readonly HashSet<string> _dependencies = new HashSet<string>();
private readonly HashSet<string> _neededKeys = new HashSet<string>();
private readonly List<string> _code = new List<string>();
private readonly List<string> _tests = new List<string>();
/// <summary>
/// A dictionary containing the implementation of basic functions
/// </summary>
/// <returns></returns>
private static IEnumerable<string> LoadFunctions(List<string> names)
{
var imps = new List<string>();
foreach (var name in names)
{
var path = $"IO/lua/{name}.lua";
if (File.Exists(path))
{
imps.Add(File.ReadAllText(path));
}
else
{
throw new FileNotFoundException(path);
}
}
return imps;
}
private void AddDep(string name)
{
_dependencies.Add(name);
}
public string ToLua()
{
var deps = _dependencies.ToList();
deps.Add("unitTestProfile");
deps.Add("inv");
deps.Add("double_compare");
var code = new List<string>();
code.Add($"-- Itinero 1.0-profile, generated on {DateTime.Now:s}");
code.Add("\n\n----------------------------- UTILS ---------------------------");
code.AddRange(LoadFunctions(deps).ToList());
code.Add("\n\n----------------------------- PROFILE ---------------------------");
var keys = _neededKeys.Select(key => "\"" + key + "\"");
code.Add("\n\nprofile_whitelist = {" + string.Join(", ", keys) + "}");
code.AddRange(_code);
code.Add("\n\n ------------------------------- TESTS -------------------------");
code.AddRange(_tests);
var compatibility = string.Join("\n",
"",
"if (itinero == nil) then",
" itinero = {}",
" itinero.log = print",
"",
" -- Itinero is not defined -> we are running from a lua interpreter -> the tests are intended",
" runTests = true",
"",
"",
"else",
" print = itinero.log",
"end",
"",
"if (not failed_tests and not failed_profile_tests) then",
" print(\"Tests OK\")",
"end"
);
code.Add(compatibility);
return string.Join("\n\n\n", code);
}
}
}

View file

@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace AspectedRouting.IO.itinero1
{
public static class LuaStringExtensions
{
public static string ToLuaTable(this Dictionary<string, string> tags)
{
var contents = tags.Select(kv =>
{
var (key, value) = kv;
var left = "[\"" + key + "\"]";
if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$"))
{
left = key;
}
return $"{left} = \"{value}\"";
});
return "{" + string.Join(", ", contents) + "}";
}
public static string FunctionName(this string s)
{
return s.Replace("$", "")
.Replace("#", "")
.Replace(" ", "_")
.Replace(".", "_")
.Replace("-", "_");
}
}
}

View file

@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
namespace AspectedRouting.IO.itinero1
{
public partial class LuaPrinter
{
private readonly HashSet<string> _alreadyAddedFunctions = new HashSet<string>();
public void AddFunction(AspectMetadata meta)
{
if (_alreadyAddedFunctions.Contains(meta.Name))
{
// already added
return;
}
_alreadyAddedFunctions.Add(meta.Name);
var possibleTags = meta.PossibleTags() ?? new Dictionary<string, List<string>>();
var numberOfCombinations = 1;
numberOfCombinations = possibleTags.Values.Select(lst => 1 + lst.Count).Multiply();
var usedParams = meta.UsedParameters();
var funcNameDeclaration = "";
meta.Visit(e =>
{
if (e is Function f && f.Name.Equals(Funcs.MemberOf.Name))
{
funcNameDeclaration = $"\n local funcName = \"{meta.Name.FunctionName()}\"";
}
return true;
});
var impl = string.Join("\n",
"--[[",
meta.Description,
"",
"Unit: " + meta.Unit,
"Created by " + meta.Author,
"Originally defined in " + meta.Filepath,
"Uses tags: " + string.Join(", ", possibleTags.Keys),
"Used parameters: " + string.Join(", ", usedParams),
"Number of combintations: " + numberOfCombinations,
"Returns values: ",
"]]",
"function " + meta.Name.FunctionName() + "(parameters, tags, result)" + funcNameDeclaration,
" return " + ToLua(meta.ExpressionImplementation),
"end"
);
_code.Add(impl);
foreach (var k in possibleTags.Keys)
{
_neededKeys.Add(k); // To generate a whitelist of OSM-keys that should be kept
}
}
}
}

View file

@ -0,0 +1,251 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
using static AspectedRouting.Language.Deconstruct;
namespace AspectedRouting.IO.itinero1
{
public partial class LuaPrinter
{
private string ToLua(IExpression bare, string key = "nil")
{
var collectedMapping = new List<IExpression>();
var order = new List<IExpression>();
if (UnApply(
UnApply(
IsFunc(Funcs.FirstOf),
Assign(order))
, UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))
).Invoke(bare))
{
AddDep(Funcs.FirstOf.Name);
return "first_match_of(tags, result, \n" +
" " + ToLua(order.First(), key) + "," +
("\n" + MappingToLua((Mapping) collectedMapping.First())).Indent().Indent() +
")";
}
if (UnApply(
UnApply(
IsFunc(Funcs.MustMatch),
Assign(order))
, UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))
).Invoke(bare))
{
AddDep(Funcs.MustMatch.Name);
return "must_match(tags, result, \n" +
" " + ToLua(order.First(), key) + "," +
("\n" + MappingToLua((Mapping) collectedMapping.First())).Indent().Indent() +
")";
}
if (UnApply(
IsFunc(Funcs.MemberOf),
Any
).Invoke(bare))
{
AddDep("memberOf");
return "member_of(funcName, parameters, tags, result)";
}
var collectedList = new List<IExpression>();
var func = new List<IExpression>();
if (
UnApply(
UnApply(IsFunc(Funcs.Dot), Assign(func)),
UnApply(IsFunc(Funcs.ListDot),
Assign(collectedList))).Invoke(bare))
{
var exprs = (IEnumerable<IExpression>) ((Constant) collectedList.First()).Evaluate(_context);
var luaExprs = new List<string>();
var funcName = func.First().ToString().TrimStart('$');
AddDep(funcName);
foreach (var expr in exprs)
{
var c = new List<IExpression>();
if (UnApply(IsFunc(Funcs.Const), Assign(c)).Invoke(expr))
{
luaExprs.Add(ToLua(c.First(), key));
continue;
}
if (expr.Types.First() is Curry curry
&& curry.ArgType.Equals(Typs.Tags))
{
var lua = ToLua(expr, key);
luaExprs.Add(lua);
}
}
return "\n " + funcName + "({\n " + string.Join(",\n ", luaExprs) +
"\n })";
}
collectedMapping.Clear();
var dottedFunction = new List<IExpression>();
dottedFunction.Clear();
if (UnApply(
UnApply(
IsFunc(Funcs.Dot),
Assign(dottedFunction)
),
UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))).Invoke(bare)
)
{
var mapping = (Mapping) collectedMapping.First();
var baseFunc = (Function) dottedFunction.First();
AddDep(baseFunc.Name);
AddDep("table_to_list");
return baseFunc.Name +
"(table_to_list(tags, result, " +
("\n" + MappingToLua(mapping)).Indent().Indent() +
"))";
}
// The expression might be a function which still expects a string (the value from the tag) as argument
if (!(bare is Mapping) &&
bare.Types.First() is Curry curr &&
curr.ArgType.Equals(Typs.String))
{
var applied = new Apply(bare, new Constant(curr.ArgType, ("tags", "\"" + key + "\"")));
return ToLua(applied.Optimize(), key);
}
// The expression might consist of multiple nested functions
var fArgs = bare.DeconstructApply();
if (fArgs != null)
{
var (f, args) = fArgs.Value;
var baseFunc = (Function) f;
AddDep(baseFunc.Name);
return baseFunc.Name + "(" + string.Join(", ", args.Select(arg => ToLua(arg, key))) + ")";
}
var collected = new List<IExpression>();
switch (bare)
{
case FunctionCall fc:
var called = _context.DefinedFunctions[fc.CalledFunctionName];
if (called.ProfileInternal)
{
return called.Name;
}
AddFunction(called);
return $"{fc.CalledFunctionName.FunctionName()}(parameters, tags, result)";
case Constant c:
return ConstantToLua(c);
case Mapping m:
return MappingToLua(m).Indent();
case Function f:
var fName = f.Name.TrimStart('$');
if (Funcs.Builtins.ContainsKey(fName))
{
AddDep(f.Name);
}
else
{
var definedFunc = _context.DefinedFunctions[fName];
if (definedFunc.ProfileInternal)
{
return f.Name;
}
AddFunction(definedFunc);
}
return f.Name;
case Apply a when UnApply(IsFunc(Funcs.Const), Assign(collected))
.Invoke(a):
return ToLua(collected.First(), key);
case Parameter p:
return $"parameters[\"{p.ParamName.FunctionName()}\"]";
default:
throw new Exception("Could not convert " + bare + " to a lua expression");
}
}
public string MappingToLua(Mapping m)
{
var contents = m.StringToResultFunctions.Select(kv =>
{
var (key, expr) = kv;
var left = "[\"" + key + "\"]";
if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$"))
{
left = key;
}
return left + " = " + ToLua(expr, key);
}
);
return
"{\n " +
string.Join(",\n ", contents) +
"\n}";
}
/// <summary>
/// Neatly creates a value expression in lua, based on a constant
/// </summary>
/// <param name="c"></param>
/// <returns></returns>
private string ConstantToLua(Constant c)
{
var o = c.Evaluate(_context);
switch (o)
{
case IExpression e:
return ConstantToLua(new Constant(e.Types.First(), e.Evaluate(null)));
case int i:
return "" + i;
case double d:
return "" + d;
case string s:
return '"' + s.Replace("\"", "\\\"") + '"';
case ValueTuple<string, string> unpack:
return unpack.Item1 + "[" + unpack.Item2 + "]";
case IEnumerable<object> ls:
var t = (c.Types.First() as ListType).InnerType;
return "{" + string.Join(", ", ls.Select(obj =>
{
var objInConstant = new Constant(t, obj);
if (obj is Constant asConstant)
{
objInConstant = asConstant;
}
return ConstantToLua(objInConstant);
})) + "}";
default:
return o.ToString();
}
}
}
}

View file

@ -0,0 +1,256 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.IO.itinero1
{
public partial class LuaPrinter
{
private readonly Context _context;
public LuaPrinter(Context context)
{
_context = context;
}
private void CreateMembershipPreprocessor(ProfileMetaData profile)
{
var memberships = Analysis.MembershipMappingsFor(profile, _context);
foreach (var (calledInFunction, membership) in memberships)
{
var funcMetaData = new AspectMetadata(
membership,
"relation_preprocessing_for_" + calledInFunction.FunctionName(),
"Function preprocessing needed for aspect " + calledInFunction +
", called by the relation preprocessor",
"Generator", "", "NA"
);
AddFunction(funcMetaData);
}
var func = new List<string>
{
"",
"",
"-- Processes the relation. All tags which are added to result.attributes_to_keep will be copied to 'attributes' of each individual way",
"function relation_tag_processor(relation_tags, result)",
" local parameters = {}",
" local subresult = {}",
" local matched = false",
" result.attributes_to_keep = {}"
};
foreach (var (calledInFunction, _) in memberships)
{
foreach (var (behaviourName, parameters) in profile.Behaviours)
{
var preProcName = "relation_preprocessing_for_" + calledInFunction.FunctionName();
func.Add("");
func.Add("");
func.Add(" subresult.attributes_to_keep = {}");
func.Add(" parameters = default_parameters()");
func.Add(ParametersToLua(parameters));
func.Add($" matched = {preProcName}(parameters, relation_tags, subresult)");
func.Add(" if (matched) then");
var tagKey = "_relation:" + behaviourName.FunctionName() + ":" + calledInFunction.FunctionName();
_neededKeys.Add("_relation:"+calledInFunction.FunctionName()); // Slightly different then tagkey!
func.Add($" result.attributes_to_keep[\"{tagKey}\"] = \"yes\"");
func.Add(" end");
}
}
func.Add("end");
_code.Add(string.Join("\n", func));
}
/// <summary>
/// Adds the necessary called functions and the profile main entry point
/// </summary>
public void AddProfile(ProfileMetaData profile)
{
var defaultParameters = "\n";
foreach (var (name, (types, inFunction)) in profile.UsedParameters(_context))
{
defaultParameters += $"{name}: {string.Join(", ", types)}\n" +
$" Used in {inFunction}\n";
}
CreateMembershipPreprocessor(profile);
var impl = string.Join("\n",
"",
"",
$"name = \"{profile.Name}\"",
"normalize = false",
"vehicle_type = {" + string.Join(", ", profile.VehicleTyps.Select(s => "\"" + s + "\"")) + "}",
"meta_whitelist = {" + string.Join(", ", profile.Metadata.Select(s => "\"" + s + "\"")) + "}",
"",
"",
"",
"--[[",
profile.Name,
"This is the main function called to calculate the access, oneway and speed.",
"Comfort is calculated as well, based on the parameters which are padded in",
"",
"Created by " + profile.Author,
"Originally defined in " + profile.Filename,
"Used parameters: " + defaultParameters.Indent(),
"]]",
"function " + profile.Name + "(parameters, tags, result)",
"",
" -- initialize the result table on the default values",
" result.access = 0",
" result.speed = 0",
" result.factor = 1",
" result.direction = 0",
" result.canstop = true",
" result.attributes_to_keep = {}",
"",
" local access = " + ToLua(profile.Access),
" if (access == nil or access == \"no\") then",
" return",
" end",
" tags.access = access",
" local oneway = " + ToLua(profile.Oneway),
" tags.oneway = oneway",
" local speed = " + ToLua(profile.Speed),
" tags.speed = speed",
" local distance = 1 -- the weight per meter for distance travelled is, well, 1m/m",
"");
impl +=
"\n local priority = \n ";
var weightParts = new List<string>();
foreach (var (parameterName, expression) in profile.Priority)
{
var priorityPart = ToLua(new Parameter(parameterName)) + " * ";
var subs = new Curry(Typs.Tags, new Var(("a"))).UnificationTable(expression.Types.First());
if (subs != null && subs.TryGetValue("$a", out var resultType) &&
(resultType.Equals(Typs.Bool) || resultType.Equals(Typs.String)))
{
priorityPart += "parse(" + ToLua(expression) + ")";
}
else
{
priorityPart += ToLua(expression);
}
weightParts.Add(priorityPart);
}
impl += string.Join(" + \n ", weightParts);
impl += string.Join("\n",
"",
"",
"",
" -- put all the values into the result-table, as needed for itinero",
" result.access = 1",
" result.speed = speed",
" result.factor = priority",
"",
" if (oneway == \"both\") then",
" result.oneway = 0",
" elseif (oneway == \"with\") then",
" result.oneway = 1",
" else",
" result.oneway = 2",
" end",
"",
"end",
"",
"",
"function default_parameters()",
" local parameters = {}",
ParametersToLua(profile.DefaultParameters),
" return parameters",
"end",
"",
""
);
var profiles = new List<string>();
foreach (var (name, subParams) in profile.Behaviours)
{
impl += BehaviourFunction(profile, name, subParams, profiles);
}
impl += "\n\n\n";
impl += "profiles = {\n {\n" +
string.Join("\n },\n {\n ", profiles) + "\n }\n}";
_code.Add(impl);
}
private string BehaviourFunction(ProfileMetaData profile,
string name,
Dictionary<string, IExpression> subParams, List<string> profiles)
{
var functionName = profile.Name + "_" + name;
subParams.TryGetValue("description", out var description);
profiles.Add(
string.Join(",\n ",
$" name = \"{name}\"",
" function_name = \"profile_" + functionName + "\"",
" metric = \"custom\""
)
);
AddDep("remove_relation_prefix");
var impl = string.Join("\n",
"",
"--[[",
description,
"]]",
"function profile_" + functionName + "(tags, result)",
$" tags = remove_relation_prefix(tags, \"{name.FunctionName()}\")",
" local parameters = default_parameters()",
""
);
impl += ParametersToLua(subParams);
impl += " " + profile.Name + "(parameters, tags, result)\n";
impl += "end\n";
return impl;
}
/// <summary>
/// `local parameters = default_parameters()` must still be invoked by caller!
/// </summary>
/// <param name="subParams"></param>
/// <returns></returns>
private string ParametersToLua(Dictionary<string, IExpression> subParams)
{
var impl = "";
foreach (var (paramName, value) in subParams)
{
if (paramName.Equals("description"))
{
continue;
}
impl += $" parameters.{paramName.TrimStart('#').FunctionName()} = {ToLua(value)}\n";
}
return impl;
}
}
}

View file

@ -0,0 +1,100 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Tests;
namespace AspectedRouting.IO.itinero1
{
public partial class LuaPrinter
{
public void AddTestSuite(ProfileTestSuite testSuite)
{
var tests = string.Join("\n",
testSuite.Tests.Select((test, i) => ToLua(testSuite, i, test.Item1, test.tags)));
_tests.Add(tests);
}
private string ToLua(ProfileTestSuite testSuite, int index, Expected expected, Dictionary<string, string> tags)
{
AddDep("debug_table");
var parameters = new Dictionary<string, string>();
var keysToCheck = new List<string>();
foreach (var (key, value) in tags)
{
if (key.StartsWith("#"))
{
parameters[key.TrimStart('#')] = value;
}
if(key.StartsWith("_relation:"))
{
keysToCheck.Add(key);
}
}
foreach (var key in keysToCheck)
{
var newKey = key.Replace(".", "_");
tags[newKey] = tags[key];
tags.Remove(key);
}
foreach (var (paramName, _) in parameters)
{
tags.Remove("#" + paramName);
}
// function unit_test_profile(profile_function, profile_name, index, expected, tags)
return $"unit_test_profile(profile_bicycle_{testSuite.BehaviourName.FunctionName()}, " +
$"\"{testSuite.BehaviourName}\", " +
$"{index}, " +
$"{{access = \"{D(expected.Access)}\", speed = {expected.Speed}, oneway = \"{D(expected.Oneway)}\", weight = {expected.Weight} }}, " +
tags.ToLuaTable() +
")";
}
private string D(string s)
{
if (string.IsNullOrEmpty(s))
{
return "0";
}
return s;
}
public void AddTestSuite(FunctionTestSuite testSuite)
{
var fName = testSuite.FunctionToApply.Name;
var tests = string.Join("\n",
testSuite.Tests.Select((test, i) => ToLua(fName, i, test.expected, test.tags)));
_tests.Add(tests);
}
private string ToLua(string functionToApplyName, int index, string expected, Dictionary<string, string> tags)
{
var parameters = new Dictionary<string, string>();
foreach (var (key, value) in tags)
{
if (key.StartsWith("#"))
{
parameters[key.TrimStart('#')] = value;
}
}
foreach (var (paramName, _) in parameters)
{
tags.Remove("#" + paramName);
}
AddDep("unitTest");
var funcName = functionToApplyName.Replace(" ", "_").Replace(".", "_");
return
$"unit_test({funcName}, \"{functionToApplyName}\", {index}, \"{expected}\", {parameters.ToLuaTable()}, {tags.ToLuaTable()})";
}
}
}

View file

@ -1,512 +1,463 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using AspectedRouting.Functions;
using AspectedRouting.Typ;
using static AspectedRouting.Functions.Funcs;
using Type = AspectedRouting.Typ.Type;
namespace AspectedRouting.IO
{
public static class JsonParser
{
public static AspectMetadata AspectFromJson(Context c, string json, string fileName)
{
try
{
var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("defaults", out _))
{
// this is a profile
return null;
}
return doc.RootElement.ParseAspect(fileName, c);
}
catch (Exception e)
{
throw new Exception("In the file " + fileName, e);
}
}
public static ProfileMetaData ProfileFromJson(Context c, string json, FileInfo f)
{
try
{
var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("defaults", out _))
{
return null;
// this is an aspect
}
return ParseProfile(doc.RootElement, c, f);
}
catch (Exception e)
{
throw new Exception("In the file " + f, e);
}
}
private static readonly IExpression _mconst =
new Apply(new Apply(Funcs.EitherFunc, Funcs.Id), Funcs.Const);
private static readonly IExpression _mappingWrapper =
new Apply(new Apply(Funcs.EitherFunc, Funcs.Id), StringStringToTags);
private static IExpression ParseMapping(IEnumerable<JsonProperty> allArgs, Context context)
{
var keys = new List<string>();
var exprs = new List<IExpression>();
foreach (var prop in allArgs)
{
if (prop.Name.Equals("#"))
{
continue;
}
keys.Add(prop.Name);
var argExpr = ParseExpression(prop.Value, context);
IExpression mappingWithOptArg;
if (argExpr.Types.Count() == 1 && argExpr.Types.First().Equals(Typs.String))
{
mappingWithOptArg =
Either(Funcs.Id, Funcs.Eq, argExpr);
}
else
{
mappingWithOptArg = new Apply(_mconst, argExpr);
}
exprs.Add(mappingWithOptArg);
}
try
{
var simpleMapping = new Mapping(keys, exprs);
return new Apply(_mappingWrapper, simpleMapping);
}
catch (Exception e)
{
throw new Exception("While constructing a mapping with members " + string.Join(", ", exprs) +
": " + e.Message, e);
}
}
private static IExpression ParseExpression(this JsonElement e, Context context)
{
if (e.ValueKind == JsonValueKind.Object)
{
// Parse an actual function
var funcCall = e.EnumerateObject().Where(v => v.Name.StartsWith("$")).ToList();
var allArgs = e.EnumerateObject().Where(v => !v.Name.StartsWith("$")).ToList();
if (funcCall.Count > 2)
{
throw new ArgumentException("Multiple calls defined in object " + e);
}
if (funcCall.Count == 1)
{
return ParseFunctionCall(context, funcCall, allArgs);
}
// Funccall has no elements: this is a mapping of strings or tags onto a value
return ParseMapping(allArgs, context);
}
if (e.ValueKind == JsonValueKind.Array)
{
var exprs = e.EnumerateArray().Select(json =>
Either(Funcs.Id, Funcs.Const, json.ParseExpression(context)));
var list = new Constant(exprs);
return Either(Funcs.Id, Funcs.ListDot, list);
}
if (e.ValueKind == JsonValueKind.Number)
{
if (e.TryGetDouble(out var d))
{
return new Constant(d);
}
if (e.TryGetInt32(out var i))
{
return new Constant(i);
}
}
if (e.ValueKind == JsonValueKind.True)
{
return new Constant(Typs.Bool, "yes");
}
if (e.ValueKind == JsonValueKind.False)
{
return new Constant(Typs.Bool, "no");
}
if (e.ValueKind == JsonValueKind.String)
{
var s = e.GetString();
if (s.StartsWith("$"))
{
var bi = BuiltinByName(s);
if (bi != null)
{
return Either(Funcs.Dot, Funcs.Id, bi);
}
var definedFunc = context.GetFunction(s);
return Either(Funcs.Dot, Funcs.Id, new FunctionCall(s, definedFunc.Types));
}
if (s.StartsWith("#"))
{
// This is a parameter, the type of it is free
return new Parameter(s);
}
return new Constant(s);
}
throw new Exception("Could not parse " + e);
}
private static IExpression ParseFunctionCall(Context context, IReadOnlyCollection<JsonProperty> funcCall,
IEnumerable<JsonProperty> allArgs)
{
var funcName = funcCall.First().Name;
var func = BuiltinByName(funcName);
// The list where all the arguments are collected
var args = new List<IExpression>();
// First argument of the function is the value of this property, e.g.
// { "$f": "xxx", "a2":"yyy", "a3":"zzz" }
var firstArgument = ParseExpression(funcCall.First().Value, context);
// Cheat for the very special case 'mustMatch'
if (func.Equals(Funcs.MustMatch))
{
// It gets an extra argument injected
var neededKeys = firstArgument.PossibleTags().Keys.ToList();
var neededKeysArg = new Constant(new ListType(Typs.String), neededKeys);
args.Add(neededKeysArg);
args.Add(firstArgument);
return func.Apply(args);
}
args.Add(firstArgument);
var allExprs = allArgs
.Where(kv => !kv.NameEquals("#")) // Leave out comments
.ToDictionary(kv => kv.Name, kv => kv.Value.ParseExpression(context));
if (allExprs.Count > 1)
{
if (func.ArgNames == null || func.ArgNames.Count < 2)
throw new ArgumentException("{funcName} does not specify argument names");
foreach (var argName in func.ArgNames)
{
args.Add(allExprs[argName]);
}
}
else if (allExprs.Count == 1)
{
args.Add(allExprs.Single().Value);
}
return Either(Funcs.Id, Funcs.Dot, func).Apply(args);
}
private static IExpression GetTopLevelExpression(this JsonElement root, Context context)
{
IExpression mapping = null;
if (root.TryGetProperty("value", out var j))
{
// The expression is placed in the default 'value' location
mapping = j.ParseExpression(context);
}
// We search for the function call with '$'
foreach (var prop in root.EnumerateObject())
{
if (!prop.Name.StartsWith("$")) continue;
var f = (IExpression) BuiltinByName(prop.Name);
if (f == null)
{
throw new KeyNotFoundException("The builtin function " + f + " was not found");
}
var fArg = prop.Value.ParseExpression(context);
if (fArg == null)
{
throw new ArgumentException("Could not type expression " + prop);
}
if (mapping != null)
{
// This is probably a firstOrderedVersion, a default, or some other function that should be applied
return
new Apply(
Either(Funcs.Id, Funcs.Dot, new Apply(f, fArg)), mapping
);
}
// Cheat for the very special case 'mustMatch'
if (f.Equals(Funcs.MustMatch))
{
// It gets an extra argument injected
var neededKeys = fArg.PossibleTags().Keys.ToList();
var neededKeysArg = new Constant(new ListType(Typs.String), neededKeys);
f = f.Apply(new[] {neededKeysArg});
}
var appliedDot = new Apply(new Apply(Funcs.Dot, f), fArg);
var appliedDirect = new Apply(f, fArg);
if (!appliedDot.Types.Any())
{
// Applied dot doesn't work out, so we return the other one
return appliedDirect;
}
if (!appliedDirect.Types.Any())
{
return appliedDot;
}
var eithered = new Apply(new Apply(Funcs.EitherFunc, appliedDot), appliedDirect);
// We apply the builtin function through a dot
return eithered;
}
throw new Exception(
"No top level reducer found. Did you forget the '$' in the reducing function? Did your forget 'value' to add the mapping?");
}
private static IExpression ParseProfileProperty(JsonElement e, Context c, string property)
{
try
{
var prop = e.GetProperty(property);
return ParseExpression(prop, c)
.Specialize(new Curry(Typs.Tags, new Var("a")))
.Optimize();
}
catch (Exception exc)
{
throw new Exception("While parsing the property " + property, exc);
}
}
private static Dictionary<string, object> ParseParameters(this JsonElement e)
{
var ps = new Dictionary<string, object>();
foreach (var obj in e.EnumerateObject())
{
var nm = obj.Name.TrimStart('#');
switch (obj.Value.ValueKind)
{
case JsonValueKind.String:
ps[nm] = obj.Value.ToString();
break;
case JsonValueKind.Number:
ps[nm] = obj.Value.GetDouble();
break;
case JsonValueKind.True:
ps[nm] = "yes";
break;
case JsonValueKind.False:
ps[nm] = "no";
break;
default:
throw new ArgumentOutOfRangeException();
}
}
return ps;
}
public static ProfileMetaData ParseProfile(this JsonElement e, Context context, FileInfo filepath)
{
var name = e.Get("name");
var author = e.TryGet("author");
if (filepath != null && !(name + ".json").ToLower().Equals(filepath.Name.ToLower()))
{
throw new ArgumentException($"Filename does not match the defined name: " +
$"filename is {filepath.Name}, declared name is {name}");
}
var vehicleTypes = e.GetProperty("vehicletypes").EnumerateArray().Select(
el => el.GetString()).ToList();
var metadata = e.GetProperty("metadata").EnumerateArray().Select(
el => el.GetString()).ToList();
var access = ParseProfileProperty(e, context, "access").Finalize();
var oneway = ParseProfileProperty(e, context, "oneway").Finalize();
var speed = ParseProfileProperty(e, context, "speed").Finalize();
IExpression TagsApplied(IExpression x)
{
return new Apply(x, new Constant(new Dictionary<string, string>()));
}
context.AddFunction("speed",
new AspectMetadata(TagsApplied(speed), "speed", "The speed of this profile", author, "", filepath.Name,
true));
context.AddFunction("access",
new AspectMetadata(TagsApplied(access), "access", "The access of this profile", author, "",
filepath.Name,
true));
context.AddFunction("oneway",
new AspectMetadata(TagsApplied(oneway), "oneway", "The oneway of this profile", author, "",
filepath.Name,
true));
context.AddFunction("distance",
new AspectMetadata(new Constant(1), "distance", "The distance travelled of this profile", author, "",
filepath.Name,
true));
var weights = new Dictionary<string, IExpression>();
var weightProperty = e.GetProperty("weight");
foreach (var prop in weightProperty.EnumerateObject())
{
var parameter = prop.Name.TrimStart('#');
var factor = ParseExpression(prop.Value, context).Finalize();
weights[parameter] = factor;
}
var profiles = new Dictionary<string, Dictionary<string, object>>();
foreach (var profile in e.GetProperty("profiles").EnumerateObject())
{
profiles[profile.Name] = ParseParameters(profile.Value);
}
return new ProfileMetaData(
name,
e.Get("description"),
author,
filepath?.DirectoryName ?? "unknown",
vehicleTypes,
e.GetProperty("defaults").ParseParameters(),
profiles,
access,
oneway,
speed,
weights,
metadata
);
}
private static AspectMetadata ParseAspect(this JsonElement e, string filepath, Context context)
{
var expr = GetTopLevelExpression(e, context);
var targetTypes = new List<Type>();
foreach (var t in expr.Types)
{
var a = Var.Fresh(t);
var b = Var.Fresh(new Curry(a, t));
if (t.Unify(new Curry(Typs.Tags, a)) != null &&
t.Unify(new Curry(Typs.Tags, new Curry(a, b))) == null
) // Second should not match
{
// The target type is 'Tags -> a', where a is NOT a curry
targetTypes.Add(t);
}
}
if (targetTypes.Count == 0)
{
throw new ArgumentException("The top level expression has types:\n" +
string.Join("\n ", expr.Types) +
"\nwhich can not be specialized into a form suiting `tags -> a`\n" + expr);
}
var exprSpec = expr.Specialize(targetTypes);
if (expr == null)
{
throw new Exception("Could not specialize the expression " + expr + " to one of the target types " +
string.Join(", ", targetTypes));
}
expr = exprSpec.Finalize();
if (expr.Finalize() == null)
{
throw new NullReferenceException("The finalized form of expression `" + exprSpec + "` is null");
}
var name = e.Get("name");
if (expr.Types.Count() > 1)
{
throw new ArgumentException("The aspect " + name + " is ambigous, it matches multiple types: " +
string.Join(", ", expr.Types));
}
if (filepath != null && !(name + ".json").ToLower().Equals(filepath.ToLower()))
{
throw new ArgumentException($"Filename does not match the defined name: " +
$"filename is {filepath}, declared name is {name}");
}
return new AspectMetadata(
expr,
name,
e.Get("description"),
e.TryGet("author"),
e.TryGet("unit"),
filepath ?? "unknown"
);
}
private static string Get(this JsonElement json, string key)
{
if (json.TryGetProperty(key, out var p))
{
return p.GetString();
}
throw new ArgumentException($"The obligated property {key} is missing");
}
private static string TryGet(this JsonElement json, string key)
{
if (json.TryGetProperty(key, out var p))
{
return p.GetString();
}
return null;
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.IO.jsonParser
{
public static partial class JsonParser
{
public static AspectMetadata AspectFromJson(Context c, string json, string fileName)
{
try
{
var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("defaults", out _))
{
// this is a profile
return null;
}
return doc.RootElement.ParseAspect(fileName, c);
}
catch (Exception e)
{
throw new Exception("In the file " + fileName, e);
}
}
public static ProfileMetaData ProfileFromJson(Context c, string json, FileInfo f)
{
try
{
var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("defaults", out _))
{
return null;
// this is an aspect
}
return ParseProfile(doc.RootElement, c, f);
}
catch (Exception e)
{
throw new Exception("In the file " + f, e);
}
}
private static ProfileMetaData ParseProfile(this JsonElement e, Context context, FileInfo filepath)
{
var name = e.Get("name");
var author = e.TryGet("author");
if (filepath != null && !(name + ".json").ToLower().Equals(filepath.Name.ToLower()))
{
throw new ArgumentException($"Filename does not match the defined name: " +
$"filename is {filepath.Name}, declared name is {name}");
}
var vehicleTypes = e.GetProperty("vehicletypes").EnumerateArray().Select(
el => el.GetString()).ToList();
var metadata = e.GetProperty("metadata").EnumerateArray().Select(
el => el.GetString()).ToList();
var access = ParseProfileProperty(e, context, "access").Finalize();
var oneway = ParseProfileProperty(e, context, "oneway").Finalize();
var speed = ParseProfileProperty(e, context, "speed").Finalize();
IExpression TagsApplied(IExpression x)
{
return new Apply(x, new Constant(new Dictionary<string, string>()));
}
context.AddFunction("speed",
new AspectMetadata(TagsApplied(speed), "speed", "The speed of this profile", author, "", filepath.Name,
true));
context.AddFunction("access",
new AspectMetadata(TagsApplied(access), "access", "The access of this profile", author, "",
filepath.Name,
true));
context.AddFunction("oneway",
new AspectMetadata(TagsApplied(oneway), "oneway", "The oneway of this profile", author, "",
filepath.Name,
true));
context.AddFunction("distance",
new AspectMetadata(new Constant(1), "distance", "The distance travelled of this profile", author, "",
filepath.Name,
true));
var weights = new Dictionary<string, IExpression>();
var weightProperty = e.GetProperty("priority");
foreach (var prop in weightProperty.EnumerateObject())
{
var parameter = prop.Name.TrimStart('#');
var factor = ParseExpression(prop.Value, context).Finalize();
weights[parameter] = factor;
}
var profiles = new Dictionary<string, Dictionary<string, IExpression>>();
foreach (var profile in e.GetProperty("behaviours").EnumerateObject())
{
profiles[profile.Name] = ParseParameters(profile.Value);
}
return new ProfileMetaData(
name,
e.Get("description"),
author,
filepath?.DirectoryName ?? "unknown",
vehicleTypes,
e.GetProperty("defaults").ParseParameters(),
profiles,
access,
oneway,
speed,
weights,
metadata
);
}
private static readonly IExpression _mconst =
new Apply(new Apply(Funcs.EitherFunc, Funcs.Id), Funcs.Const);
private static readonly IExpression _mappingWrapper =
new Apply(new Apply(Funcs.EitherFunc, Funcs.Id), Funcs.StringStringToTags);
private static IExpression ParseMapping(IEnumerable<JsonProperty> allArgs, Context context)
{
var keys = new List<string>();
var exprs = new List<IExpression>();
foreach (var prop in allArgs)
{
if (prop.Name.Equals("#"))
{
continue;
}
keys.Add(prop.Name);
var argExpr = ParseExpression(prop.Value, context);
IExpression mappingWithOptArg;
if (argExpr.Types.Count() == 1 && argExpr.Types.First().Equals(Typs.String))
{
mappingWithOptArg =
Funcs.Either(Funcs.Id, Funcs.Eq, argExpr);
}
else
{
mappingWithOptArg = new Apply(_mconst, argExpr);
}
exprs.Add(mappingWithOptArg);
}
try
{
var simpleMapping = new Mapping(keys, exprs);
return new Apply(_mappingWrapper, simpleMapping);
}
catch (Exception e)
{
throw new Exception("While constructing a mapping with members " + string.Join(", ", exprs) +
": " + e.Message, e);
}
}
private static IExpression ParseExpression(this JsonElement e, Context context)
{
if (e.ValueKind == JsonValueKind.Object)
{
// Parse an actual function
var funcCall = e.EnumerateObject().Where(v => v.Name.StartsWith("$")).ToList();
var allArgs = e.EnumerateObject().Where(v => !v.Name.StartsWith("$")).ToList();
if (funcCall.Count > 2)
{
throw new ArgumentException("Multiple calls defined in object " + e);
}
if (funcCall.Count == 1)
{
return ParseFunctionCall(context, funcCall, allArgs);
}
// Funccall has no elements: this is a mapping of strings or tags onto a value
return ParseMapping(allArgs, context);
}
if (e.ValueKind == JsonValueKind.Array)
{
var exprs = e.EnumerateArray().Select(json =>
Funcs.Either(Funcs.Id, Funcs.Const, json.ParseExpression(context)));
var list = new Constant(exprs);
return Funcs.Either(Funcs.Id, Funcs.ListDot, list);
}
if (e.ValueKind == JsonValueKind.Number)
{
if (e.TryGetDouble(out var d))
{
return new Constant(d);
}
if (e.TryGetInt32(out var i))
{
return new Constant(i);
}
}
if (e.ValueKind == JsonValueKind.True)
{
return new Constant(Typs.Bool, "yes");
}
if (e.ValueKind == JsonValueKind.False)
{
return new Constant(Typs.Bool, "no");
}
if (e.ValueKind == JsonValueKind.String)
{
var s = e.GetString();
if (s.StartsWith("$"))
{
var bi = Funcs.BuiltinByName(s);
if (bi != null)
{
return Funcs.Either(Funcs.Dot, Funcs.Id, bi);
}
var definedFunc = context.GetFunction(s);
return Funcs.Either(Funcs.Dot, Funcs.Id, new FunctionCall(s, definedFunc.Types));
}
if (s.StartsWith("#"))
{
// This is a parameter, the type of it is free
return new Parameter(s);
}
return new Constant(s);
}
throw new Exception("Could not parse " + e);
}
private static IExpression ParseFunctionCall(Context context, IReadOnlyCollection<JsonProperty> funcCall,
IEnumerable<JsonProperty> allArgs)
{
var funcName = funcCall.First().Name;
var func = Funcs.BuiltinByName(funcName);
if (func == null)
{
throw new ArgumentException($"The function with name {funcName} is not found");
}
// The list where all the arguments are collected
var args = new List<IExpression>();
// First argument of the function is the value of this property, e.g.
// { "$f": "xxx", "a2":"yyy", "a3":"zzz" }
var firstArgument = ParseExpression(funcCall.First().Value, context);
// Cheat for the very special case 'mustMatch'
if (func.Equals(Funcs.MustMatch))
{
// It gets an extra argument injected
var neededKeys = firstArgument.PossibleTags().Keys.ToList();
var neededKeysArg = new Constant(new ListType(Typs.String), neededKeys);
args.Add(neededKeysArg);
args.Add(firstArgument);
return func.Apply(args);
}
args.Add(firstArgument);
var allExprs = allArgs
.Where(kv => !kv.NameEquals("#")) // Leave out comments
.ToDictionary(kv => kv.Name, kv => kv.Value.ParseExpression(context));
if (allExprs.Count > 1)
{
if (func.ArgNames == null || func.ArgNames.Count < 2)
throw new ArgumentException("{funcName} does not specify argument names");
foreach (var argName in func.ArgNames)
{
args.Add(allExprs[argName]);
}
}
else if (allExprs.Count == 1)
{
args.Add(allExprs.Single().Value);
}
return Funcs.Either(Funcs.Id, Funcs.Dot, func).Apply(args);
}
private static IExpression GetTopLevelExpression(this JsonElement root, Context context)
{
IExpression mapping = null;
if (root.TryGetProperty("value", out var j))
{
// The expression is placed in the default 'value' location
mapping = j.ParseExpression(context);
}
// We search for the function call with '$'
foreach (var prop in root.EnumerateObject())
{
if (!prop.Name.StartsWith("$")) continue;
var f = (IExpression) Funcs.BuiltinByName(prop.Name);
if (f == null)
{
throw new KeyNotFoundException($"The builtin function {prop.Name} was not found");
}
var fArg = prop.Value.ParseExpression(context);
if (fArg == null)
{
throw new ArgumentException("Could not type expression " + prop);
}
if (mapping != null)
{
// This is probably a firstOrderedVersion, a default, or some other function that should be applied
return
new Apply(
Funcs.Either(Funcs.Id, Funcs.Dot, new Apply(f, fArg)), mapping
);
}
// Cheat for the very special case 'mustMatch'
if (f.Equals(Funcs.MustMatch))
{
// It gets an extra argument injected
var neededKeys = fArg.PossibleTags().Keys.ToList();
var neededKeysArg = new Constant(new ListType(Typs.String), neededKeys);
f = f.Apply(new[] {neededKeysArg});
}
var appliedDot = new Apply(new Apply(Funcs.Dot, f), fArg);
var appliedDirect = new Apply(f, fArg);
if (!appliedDot.Types.Any())
{
// Applied dot doesn't work out, so we return the other one
return appliedDirect;
}
if (!appliedDirect.Types.Any())
{
return appliedDot;
}
var eithered = new Apply(new Apply(Funcs.EitherFunc, appliedDot), appliedDirect);
// We apply the builtin function through a dot
return eithered;
}
throw new Exception(
"No top level reducer found. Did you forget the '$' in the reducing function? Did your forget 'value' to add the mapping?");
}
private static AspectMetadata ParseAspect(this JsonElement e, string filepath, Context context)
{
var expr = GetTopLevelExpression(e, context);
var targetTypes = new List<Type>();
foreach (var t in expr.Types)
{
var a = Var.Fresh(t);
var b = Var.Fresh(new Curry(a, t));
if (t.Unify(new Curry(Typs.Tags, a)) != null &&
t.Unify(new Curry(Typs.Tags, new Curry(a, b))) == null
) // Second should not match
{
// The target type is 'Tags -> a', where a is NOT a curry
targetTypes.Add(t);
}
}
if (targetTypes.Count == 0)
{
throw new ArgumentException("The top level expression has types:\n" +
string.Join("\n ", expr.Types) +
"\nwhich can not be specialized into a form suiting `tags -> a`\n" + expr);
}
var exprSpec = expr.Specialize(targetTypes);
if (expr == null)
{
throw new Exception("Could not specialize the expression " + expr + " to one of the target types " +
string.Join(", ", targetTypes));
}
expr = exprSpec.Finalize();
if (expr.Finalize() == null)
{
throw new NullReferenceException("The finalized form of expression `" + exprSpec + "` is null");
}
var name = e.Get("name");
if (expr.Types.Count() > 1)
{
throw new ArgumentException("The aspect " + name + " is ambigous, it matches multiple types: " +
string.Join(", ", expr.Types));
}
if (filepath != null && !(name + ".json").ToLower().Equals(filepath.ToLower()))
{
throw new ArgumentException($"Filename does not match the defined name: " +
$"filename is {filepath}, declared name is {name}");
}
var keys = (IEnumerable<string>) expr.PossibleTags()?.Keys ?? new List<string>();
foreach (var key in keys)
{
if (!key.Trim().Equals(key))
{
Console.WriteLine($"Warning: a key can be trimmed: '{key}'");
}
}
return new AspectMetadata(
expr,
name,
e.Get("description"),
e.TryGet("author"),
e.TryGet("unit"),
filepath ?? "unknown"
);
}
}
}

View file

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.IO.jsonParser
{
public static partial class JsonParser
{
private static IExpression ParseProfileProperty(JsonElement e, Context c, string property)
{
try
{
var prop = e.GetProperty(property);
return ParseExpression(prop, c)
.Specialize(new Curry(Typs.Tags, new Var("a")))
.Optimize();
}
catch (Exception exc)
{
throw new Exception("While parsing the property " + property, exc);
}
}
private static Dictionary<string, IExpression> ParseParameters(this JsonElement e)
{
var ps = new Dictionary<string, IExpression>();
foreach (var obj in e.EnumerateObject())
{
var nm = obj.Name.TrimStart('#');
switch (obj.Value.ValueKind)
{
case JsonValueKind.String:
var v = obj.Value.ToString();
if (v.Equals("yes") || v.Equals("no"))
{
ps[nm] = new Constant(Typs.Bool, v);
}
else
{
ps[nm] = new Constant(v);
}
break;
case JsonValueKind.Number:
ps[nm] = new Constant(obj.Value.GetDouble());
break;
case JsonValueKind.True:
ps[nm] = new Constant(Typs.Bool, "yes");
break;
case JsonValueKind.False:
ps[nm] = new Constant(Typs.Bool, "yes");
break;
case JsonValueKind.Array:
var list = obj.Value.EnumerateArray().Select(e => e.ToString()).ToList();
ps[nm] = new Constant(new ListType(Typs.String),list);
break;
default:
throw new ArgumentException(
"Parameters are not allowed to be complex expressions, they should be simple values. " +
"Simplify the value for parameter " + obj.Name);
}
}
return ps;
}
private static string Get(this JsonElement json, string key)
{
if (json.TryGetProperty(key, out var p))
{
return p.GetString();
}
throw new ArgumentException($"The obligated property {key} is missing");
}
private static string TryGet(this JsonElement json, string key)
{
if (json.TryGetProperty(key, out var p))
{
return p.GetString();
}
return null;
}
}
}

View file

@ -0,0 +1,3 @@
function const(a, b)
return a
end

View file

@ -0,0 +1,9 @@
function containedIn(list, a)
for _, value in ipairs(list) do
if (value == a) then
return true
end
end
return false;
end

View file

@ -1,4 +1,8 @@
function double_compare(a, b)
if (b == nil) then
return false
end
if (type(a) ~= "number") then
a = parse(a)
end
@ -6,5 +10,9 @@ function double_compare(a, b)
if(type(b) ~= "number") then
b = parse(b)
end
return math.abs(a - b) > 0.001
if (a == b) then
return true
end
return math.abs(a - b) < 0.0001
end

View file

@ -1,3 +1,10 @@
function member_of()
???
function member_of(calledIn, parameters, tags, result)
local k = "_relation:" .. calledIn
-- This tag is conventiently setup by all the preprocessors, which take the parameters into account
local doesMatch = tags[k]
if (doesMatch == "yes") then
result.attributes_to_keep[k] = "yes"
return true
end
return false
end

View file

@ -1,5 +1,4 @@
function must_match(tags, result, needed_keys, table)
local result_list = {}
for _, key in ipairs(needed_keys) do
local v = tags[key]
if (v == nil) then
@ -9,17 +8,30 @@ function must_match(tags, result, needed_keys, table)
local mapping = table[key]
if (type(mapping) == "table") then
local resultValue = mapping[v]
if (v == nil or v == false) then
if (resultValue == nil or
resultValue == false or
resultValue == "no" or
resultValue == "false") then
return false
end
if (v == "no" or v == "false") then
elseif (type(mapping) == "string") then
local bool = mapping
if (bool == "yes" or bool == "1") then
return true
elseif (bool == "no" or bool == "0") then
return false
end
result.attributes_to_keep[key] = v
error("MustMatch got a string value it can't handle: " .. bool)
else
error("The mapping is not a table. This is not supported")
error("The mapping is not a table. This is not supported. We got " .. mapping)
end
end
-- Now that we know for sure that every key matches, we add them all
for _, key in ipairs(needed_keys) do
local v = tags[key]
result.attributes_to_keep[key] = v
end
return true;
end

View file

@ -1,4 +1,8 @@
function notEq(a, b)
if (b == nil) then
b = "yes"
end
if (a ~= b) then
return "yes"
else

View file

@ -0,0 +1,20 @@
function string.start(strt, s)
return string.sub(s, 1, string.len(strt)) == strt
end
-- every key starting with "_relation:<name>:XXX" is rewritten to "_relation:XXX"
function remove_relation_prefix(tags, name)
local new_tags = {}
for k, v in pairs(tags) do
local prefix = "_relation:" .. name;
if (string.start(prefix, k)) then
local new_key = "_relation:" .. string.sub(k, string.len(prefix))
new_tags[new_key] = v
else
new_tags[k] = v
end
end
return new_tags
end

View file

@ -3,36 +3,57 @@ failed_profile_tests = false
expected should be a table containing 'access', 'speed' and 'weight'
]]
function unit_test_profile(profile_function, profile_name, index, expected, tags)
result = {attributes_to_keep = {}}
local result = { attributes_to_keep = {} }
local profile_failed = false
profile_function(tags, result)
if (result.access ~= expected.access) then
local accessCorrect = (result.access == 0 and expected.access == "no") or result.access == 1
if (not accessCorrect) then
print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".access: expected " .. expected.access .. " but got " .. result.access)
profile_failed = true
failed_profile_tests = true
end
if (result.access == 0) then
if (expected.access == "no") then
-- we cannot access this road, the other results are irrelevant
if (profile_failed) then
print("The used tags for test " .. tostring(index) .. " are:")
debug_table(tags)
end
return
end
if (double_compare(result.speed, expected.speed)) then
if (not double_compare(result.speed, expected.speed)) then
print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".speed: expected " .. expected.speed .. " but got " .. result.speed)
failed_profile_tests = true
profile_failed = true
end
if (double_compare(result.oneway, expected.oneway)) then
print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".oneway: expected " .. expected.oneway .. " but got " .. result.oneway)
failed_profile_tests = true
local actualOneway = result.oneway;
if (result.oneway == 0) then
actualOneway = "both"
elseif (result.oneway == 1) then
actualOneway = "with"
elseif (result.oneway == 2) then
actualOneway = "against"
end
if (double_compare(result.oneway, expected.oneway)) then
print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".oneway: expected " .. expected.oneway .. " but got " .. result.oneway)
if (expected.oneway ~= actualOneway) then
print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".oneway: expected " .. expected.oneway .. " but got " .. actualOneway)
failed_profile_tests = true
profile_failed = true
end
if (double_compare(inv(result.factor), 0.033333)) then
print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".factor: expected " .. expected.weight .. " but got " .. inv(result.factor))
if (not double_compare(result.factor, expected.weight)) then
print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".factor: expected " .. expected.weight .. " but got " .. result.factor)
failed_profile_tests = true
profile_failed = true
end
if (profile_failed == true) then
print("The used tags for test " .. tostring(index) .. " are:")
debug_table(tags)
end
end

View file

@ -17,6 +17,7 @@
- parse
- to_string
- concat
- containedIn
- min
- max
- sum
@ -40,7 +41,7 @@
#### eq
Argument name | | |
--------------| - | - | -
-------------- | - | - | -
**a** | $a | $a |
**b** | $a | $a |
_return type_ | bool | string |
@ -66,14 +67,14 @@ end
#### notEq
Argument name | | |
--------------| - | - | -
**a** | $a | $a |
**b** | $a | $a |
_return type_ | bool | string |
Argument name | | | |
-------------- | - | - | - | -
**a** | $a | $a | bool |
**b** | $a | $a | _none_ |
_return type_ | bool | string | bool |
Returns 'yes' if the two passed in values are _not_ the same
OVerloaded function, either boolean not or returns 'yes' if the two passed in values are _not_ the same;
@ -81,6 +82,10 @@ Lua implementation:
````lua
function notEq(a, b)
if (b == nil) then
b = "yes"
end
if (a ~= b) then
return "yes"
else
@ -92,14 +97,14 @@ end
#### not
Argument name | | |
--------------| - | - | -
**a** | $a | $a |
**b** | $a | $a |
_return type_ | bool | string |
Argument name | | | |
-------------- | - | - | - | -
**a** | $a | $a | bool |
**b** | $a | $a | _none_ |
_return type_ | bool | string | bool |
Returns 'yes' if the two passed in values are _not_ the same
OVerloaded function, either boolean not or returns 'yes' if the two passed in values are _not_ the same;
@ -107,6 +112,10 @@ Lua implementation:
````lua
function notEq(a, b)
if (b == nil) then
b = "yes"
end
if (a ~= b) then
return "yes"
else
@ -119,7 +128,7 @@ end
#### inv
Argument name | | |
--------------| - | - | -
-------------- | - | - | -
**d** | pdouble | double |
_return type_ | pdouble | double |
@ -140,7 +149,7 @@ end
#### default
Argument name | |
--------------| - | -
-------------- | - | -
**defaultValue** | $a |
**f** | $b -> $a |
_return type_ | $b -> $a |
@ -164,10 +173,10 @@ end
#### parse
Argument name | |
--------------| - | -
**s** | string |
_return type_ | double |
Argument name | | |
-------------- | - | - | -
**s** | string | string |
_return type_ | double | pdouble |
Parses a string into a numerical value
@ -210,7 +219,7 @@ end
#### to_string
Argument name | |
--------------| - | -
-------------- | - | -
**obj** | $a |
_return type_ | string |
@ -231,7 +240,7 @@ end
#### concat
Argument name | |
--------------| - | -
-------------- | - | -
**a** | string |
**b** | string |
_return type_ | string |
@ -250,10 +259,38 @@ end
````
#### containedIn
Argument name | |
-------------- | - | -
**list** | list ($a) |
**a** | $a |
_return type_ | bool |
Given a list of values, checks if the argument is contained in the list.
Lua implementation:
````lua
function containedIn(list, a)
for _, value in ipairs(list) do
if (value == a) then
return true
end
end
return false;
end
````
#### min
Argument name | | | | | |
--------------| - | - | - | - | - | -
-------------- | - | - | - | - | - | -
**list** | list (nat) | list (int) | list (pdouble) | list (double) | list (bool) |
_return type_ | nat | int | pdouble | double | bool |
@ -283,7 +320,7 @@ end
#### max
Argument name | | | | | |
--------------| - | - | - | - | - | -
-------------- | - | - | - | - | - | -
**list** | list (nat) | list (int) | list (pdouble) | list (double) | list (bool) |
_return type_ | nat | int | pdouble | double | bool |
@ -313,7 +350,7 @@ end
#### sum
Argument name | | | | | |
--------------| - | - | - | - | - | -
-------------- | - | - | - | - | - | -
**list** | list (nat) | list (int) | list (pdouble) | list (double) | list (bool) |
_return type_ | nat | int | pdouble | double | int |
@ -341,7 +378,7 @@ end
#### multiply
Argument name | | | | | |
--------------| - | - | - | - | - | -
-------------- | - | - | - | - | - | -
**list** | list (nat) | list (int) | list (pdouble) | list (double) | list (bool) |
_return type_ | nat | int | pdouble | double | bool |
@ -366,7 +403,7 @@ end
#### firstMatchOf
Argument name | |
--------------| - | -
-------------- | - | -
**s** | list (string) |
_return type_ | (tags -> list ($a)) -> tags -> $a |
@ -404,7 +441,7 @@ end
#### mustMatch
Argument name | |
--------------| - | -
-------------- | - | -
**neededKeys (filled in by parser)** | list (string) |
**f** | tags -> list (bool) |
_return type_ | tags -> bool |
@ -420,7 +457,6 @@ Lua implementation:
````lua
function must_match(tags, result, needed_keys, table)
local result_list = {}
for _, key in ipairs(needed_keys) do
local v = tags[key]
if (v == nil) then
@ -430,18 +466,31 @@ function must_match(tags, result, needed_keys, table)
local mapping = table[key]
if (type(mapping) == "table") then
local resultValue = mapping[v]
if (v == nil or v == false) then
if (resultValue == nil or
resultValue == false or
resultValue == "no" or
resultValue == "false") then
return false
end
if (v == "no" or v == "false") then
elseif (type(mapping) == "string") then
local bool = mapping
if (bool == "yes" or bool == "1") then
return true
elseif (bool == "no" or bool == "0") then
return false
end
result.attributes_to_keep[key] = v
error("MustMatch got a string value it can't handle: " .. bool)
else
error("The mapping is not a table. This is not supported")
error("The mapping is not a table. This is not supported. We got " .. mapping)
end
end
-- Now that we know for sure that every key matches, we add them all
for _, key in ipairs(needed_keys) do
local v = tags[key]
result.attributes_to_keep[key] = v
end
return true;
end
````
@ -449,22 +498,41 @@ end
#### memberOf
- (tags -> $a) -> tags -> list ($a)
Argument name | |
-------------- | - | -
**f** | tags -> bool |
**tags** | tags |
_return type_ | bool |
This function uses memberships of relations to calculate values.
Consider all the relations the scrutinized way is part of.The enclosed function is executed for every single relation which is part of it, generating a list of results.This list of results is in turn returned by 'memberOf'
In itinero 1/lua, this is implemented by converting the matching relations and by adding the tags of the relations to the dictionary (or table) with the highway tags.The prefix is '_relation:n:key=value', where 'n' is a value between 0 and the number of matching relations (implying that all of these numbers are scanned).The matching relations can be extracted by the compiler for the preprocessing.
This function returns true, if the way is member of a relation matching the specified function.
For testing, the relation can be emulated by using e.g. '_relation:0:key=value'
In order to use this for itinero 1.0, the membership _must_ be the top level expression.
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 test.csv, one can simply use `_relation:<aspect_name>=yes` to mimic relations in your tests
Lua implementation:
````lua
function member_of()
???
function member_of(calledIn, parameters, tags, result)
local k = "_relation:" .. calledIn
-- This tag is conventiently setup by all the preprocessors, which take the parameters into account
local doesMatch = tags[k]
if (doesMatch == "yes") then
result.attributes_to_keep[k] = "yes"
return true
end
return false
end
````
@ -472,7 +540,7 @@ end
#### if_then_else
Argument name | | |
--------------| - | - | -
-------------- | - | - | -
**condition** | bool | bool |
**then** | $a | $a |
**else** | $a | _none_ |
@ -499,7 +567,7 @@ end
#### if
Argument name | | |
--------------| - | - | -
-------------- | - | - | -
**condition** | bool | bool |
**then** | $a | $a |
**else** | $a | _none_ |
@ -526,7 +594,7 @@ end
#### id
Argument name | |
--------------| - | -
-------------- | - | -
**a** | $a |
_return type_ | $a |
@ -547,7 +615,7 @@ end
#### const
Argument name | |
--------------| - | -
-------------- | - | -
**a** | $a |
**b** | $b |
_return type_ | $a |
@ -560,14 +628,16 @@ Small utility function, which takes two arguments `a` and `b` and returns `a`. U
Lua implementation:
````lua
function const(a, b)
return a
end
````
#### constRight
Argument name | |
--------------| - | -
-------------- | - | -
**a** | $a |
**b** | $b |
_return type_ | $b |
@ -587,11 +657,11 @@ Lua implementation:
#### dot
Argument name | |
--------------| - | -
**f** | $gType -> $arg |
**g** | $fType -> $gType |
**a** | $fType |
_return type_ | $arg |
-------------- | - | -
**f** | $b -> $c |
**g** | $a -> $b |
**a** | $a |
_return type_ | $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
@ -608,7 +678,7 @@ Lua implementation:
#### listDot
Argument name | |
--------------| - | -
-------------- | - | -
**list** | list ($a -> $b) |
**a** | $a |
_return type_ | list ($b) |
@ -628,10 +698,19 @@ Lua implementation:
#### eitherFunc
- ($a -> $b) -> ($c -> $d) -> $a -> $b
- ($a -> $b) -> ($c -> $d) -> $c -> $d
Argument name | | |
-------------- | - | - | -
**f** | $a -> $b | $a -> $b |
**g** | $c -> $d | $c -> $d |
**a** | $a | $c |
_return type_ | $b | $d |
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.
Disclaimer: _you should never ever need this in your profiles_
@ -644,9 +723,14 @@ Lua implementation:
#### stringToTags
- (string -> string -> $a) -> tags -> list ($a)
Argument name | |
-------------- | - | -
**f** | string -> string -> $a |
**tags** | tags |
_return type_ | list ($a) |
stringToTags converts a function `string -> string -> a` into a function `tags -> [a]`

View file

@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Functions;
using AspectedRouting.Typ;
using static AspectedRouting.Deconstruct;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting
namespace AspectedRouting.Language
{
public static class Analysis
{
@ -26,6 +27,7 @@ namespace AspectedRouting
Console.WriteLine(e);
var keys = e.PossibleTags().Keys.ToList();
var results = possibleTags.OnAllCombinations(
tags =>
{
@ -124,10 +126,10 @@ namespace AspectedRouting
} while (SelectNext());
}
public static Dictionary<string, (IEnumerable<Typ.Type> Types, string inFunction)> UsedParameters(
public static Dictionary<string, (List<Type> Types, string inFunction)> UsedParameters(
this ProfileMetaData profile, Context context)
{
var parameters = new Dictionary<string, (IEnumerable<Typ.Type> Types, string inFunction)>();
var parameters = new Dictionary<string, (List<Type> Types, string inFunction)>();
void AddParams(IExpression e, string inFunction)
{
@ -149,7 +151,7 @@ namespace AspectedRouting
}
else
{
parameters[param.ParamName] = (param.Types, inFunction);
parameters[param.ParamName] = (param.Types.ToList(), inFunction);
}
}
}
@ -159,6 +161,12 @@ namespace AspectedRouting
AddParams(profile.Oneway, profile.Name + ".oneway");
AddParams(profile.Speed, profile.Name + ".speed");
foreach (var (key, expr) in profile.Priority)
{
AddParams(new Parameter(key), profile.Name + ".priority.lefthand");
AddParams(expr, profile.Name + ".priority");
}
foreach (var (name, expr) in context.DefinedFunctions)
{
AddParams(expr, name);
@ -194,11 +202,11 @@ namespace AspectedRouting
return text;
}
public static void SanityCheckProfile(this ProfileMetaData pmd)
public static void SanityCheckProfile(this ProfileMetaData pmd, Context context)
{
var defaultParameters = pmd.DefaultParameters.Keys;
var usedParameters = pmd.UsedParameters(new Context()).Keys.Select(key => key.TrimStart('#'));
var usedParameters = pmd.UsedParameters(context).Keys.Select(key => key.TrimStart('#'));
var diff = usedParameters.ToHashSet().Except(defaultParameters).ToList();
if (diff.Any())
@ -206,28 +214,48 @@ namespace AspectedRouting
throw new ArgumentException("No default value set for parameter " + string.Join(", ", diff));
}
foreach (var (profileName, profileParams) in pmd.Profiles)
var unused = defaultParameters.Except(usedParameters);
if (unused.Any())
{
throw new ArgumentException("A default value is set for parameter, but it is unused: " +
string.Join(", ", unused));
}
foreach (var (behaviourName, behaviourParams) in pmd.Behaviours)
{
var sum = 0.0;
foreach (var (paramName, _) in pmd.Weights)
var explanation = "";
foreach (var (paramName, _) in pmd.Priority)
{
if (!profileParams.TryGetValue(paramName, out var weight))
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))
{
explanation += $"\n - {paramName} = default (not set)";
continue;
}
if (!(weight is double d))
var weightObj = weight.Evaluate(context);
if (!(weightObj is double d))
{
continue;
throw new ArgumentException($"The parameter {paramName} is not a numeric value");
}
sum += Math.Abs(d);
explanation += $"\n - {paramName} = {d}";
}
if (Math.Abs(sum) < 0.0001)
{
throw new ArgumentException("Profile " + profileName +
": the summed parameters to calculate the weight are zero or very low");
throw new ArgumentException("Profile " + behaviourName +
": the summed parameters to calculate the weight are zero or very low:" +
explanation);
}
}
}
@ -238,9 +266,9 @@ namespace AspectedRouting
{
var order = new List<IExpression>();
var mapping = new List<IExpression>();
if (UnApply(
UnApply(IsFunc(Funcs.FirstOf), Assign(order)),
Assign(mapping)
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 =>
@ -339,5 +367,61 @@ namespace AspectedRouting
return result;
}
public static Dictionary<string, IExpression> MembershipMappingsFor(ProfileMetaData profile, Context context)
{
var calledFunctions = profile.Priority.Values.ToHashSet();
calledFunctions.Add(profile.Speed);
calledFunctions.Add(profile.Access);
calledFunctions.Add(profile.Oneway);
var calledFunctionQueue = new Queue<string>();
var alreadyAnalysedFunctions = new HashSet<string>();
var memberships = new Dictionary<string, IExpression>();
void HandleExpression(IExpression e, string calledIn)
{
e.Visit(f =>
{
var mapping = new List<IExpression>();
if (Deconstruct.UnApply(Deconstruct.IsFunc(Funcs.MemberOf),
Deconstruct.Assign(mapping)
).Invoke(f))
{
memberships.Add(calledIn, mapping.First());
return false;
}
if (f is FunctionCall fc)
{
calledFunctionQueue.Enqueue(fc.CalledFunctionName);
}
return true;
});
}
foreach (var e in calledFunctions)
{
HandleExpression(e, "profile_root");
}
while (calledFunctionQueue.TryDequeue(out var functionName))
{
if (alreadyAnalysedFunctions.Contains(functionName))
{
continue;
}
alreadyAnalysedFunctions.Add(functionName);
var functionImplementation = context.GetFunction(functionName);
HandleExpression(functionImplementation, functionName);
}
return memberships;
}
}
}

View file

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
namespace AspectedRouting.Language
{
public class Context
{
public readonly Dictionary<string, IExpression> Parameters = new Dictionary<string, IExpression>();
public readonly Dictionary<string, AspectMetadata> DefinedFunctions = new Dictionary<string, AspectMetadata>();
public readonly string AspectName;
public Context()
{
}
protected Context(string aspectName, Dictionary<string, IExpression> parameters,
Dictionary<string, AspectMetadata> definedFunctions)
{
AspectName = aspectName;
Parameters = parameters;
DefinedFunctions = definedFunctions;
}
public Context(Context c) : this(c.AspectName, c.Parameters, c.DefinedFunctions)
{
}
public void AddParameter(string name, string value)
{
Parameters.Add(name, new Constant(value));
}
public void AddFunction(string name, AspectMetadata function)
{
if (Funcs.Builtins.ContainsKey(name))
{
throw new ArgumentException("Function " + name + " already exists, it is a builtin function");
}
if (DefinedFunctions.ContainsKey(name) && !function.ProfileInternal)
{
throw new ArgumentException("Function " + name + " already exists");
}
DefinedFunctions[name] = function;
}
public IExpression GetFunction(string name)
{
if (name.StartsWith("$"))
{
name = name.Substring(1);
}
if (Funcs.Builtins.ContainsKey(name))
{
return Funcs.Builtins[name];
}
if (DefinedFunctions.ContainsKey(name))
{
return DefinedFunctions[name];
}
throw new ArgumentException(
$"The function {name} is not a defined nor builtin function. Known functions are " +
string.Join(", ", DefinedFunctions.Keys));
}
public Context WithParameters(Dictionary<string, IExpression> parameters)
{
return new Context(AspectName, parameters, DefinedFunctions);
}
public Context WithAspectName(string name)
{
return new Context(name, Parameters, DefinedFunctions);
}
}
}

View file

@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Functions;
using AspectedRouting.Language.Expression;
namespace AspectedRouting
namespace AspectedRouting.Language
{
public static class Deconstruct
{
@ -99,9 +99,7 @@ namespace AspectedRouting
};
}
public static Func<IExpression, bool> Any()
{
return e => true;
}
public static readonly Func<IExpression, bool> Any = e => true;
}
}

View file

@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using static AspectedRouting.Deconstruct;
using Type = AspectedRouting.Typ.Type;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Expression
{
public class Apply : IExpression
{
@ -88,7 +88,7 @@ namespace AspectedRouting.Functions
$"is applied on an argument with types:" +
$"{string.Join(", ", argument.Optimize().Types)}";
}
catch (Exception e)
catch (Exception)
{
_debugInfo = $"\n{f.TypeBreakdown()}\n" +
$"{argument.TypeBreakdown()}";
@ -99,9 +99,9 @@ namespace AspectedRouting.Functions
public object Evaluate(Context c, params IExpression[] arguments)
{
if (Types.Count() > 1)
if (!Types.Any())
{
// We try to select the smallest type
throw new ArgumentException("Trying to invoke an invalid expression: " + this);
}
var type = Types.First();
@ -166,20 +166,20 @@ namespace AspectedRouting.Functions
// => (const dot _) id => dot id => id
// or => (constRight _ id) id => id id => id
if (
UnApplyAny(
UnApplyAny(
UnApplyAny(
IsFunc(Funcs.Const),
IsFunc(Funcs.Dot)),
Any()),
IsFunc(Funcs.Id)
Deconstruct.UnApplyAny(
Deconstruct.UnApplyAny(
Deconstruct.UnApplyAny(
Deconstruct.IsFunc(Funcs.Const),
Deconstruct.IsFunc(Funcs.Dot)),
Deconstruct.Any),
Deconstruct.IsFunc(Funcs.Id)
).Invoke(this)
&& UnApplyAny(UnApplyAny(
UnApplyAny(
IsFunc(Funcs.ConstRight),
Any()),
IsFunc(Funcs.Id)),
IsFunc(Funcs.Id)
&& Deconstruct.UnApplyAny(Deconstruct.UnApplyAny(
Deconstruct.UnApplyAny(
Deconstruct.IsFunc(Funcs.ConstRight),
Deconstruct.Any),
Deconstruct.IsFunc(Funcs.Id)),
Deconstruct.IsFunc(Funcs.Id)
).Invoke(this))
{
return Funcs.Id;
@ -247,12 +247,12 @@ namespace AspectedRouting.Functions
// ((dot f0) f1)
// ((dot f0) f1) arg is the actual expression, but arg is already split of
if (UnApply(
UnApply(
IsFunc(Funcs.Dot),
Assign(f0)
if (Deconstruct.UnApply(
Deconstruct.UnApply(
Deconstruct.IsFunc(Funcs.Dot),
Deconstruct.Assign(f0)
),
Assign(f1)).Invoke(f)
Deconstruct.Assign(f1)).Invoke(f)
)
{
// f0 (f1 arg)

View file

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using Type = AspectedRouting.Typ.Type;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Expression
{
public class AspectMetadata : IExpression
{
@ -32,6 +32,12 @@ namespace AspectedRouting.Functions
public object Evaluate(Context c, params IExpression[] arguments)
{
if (ProfileInternal && arguments.Length > 0)
{
var tags = (Dictionary<string, string>) arguments[0].Evaluate(c);
}
return ExpressionImplementation.Evaluate(c, arguments);
}

View file

@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using Type = AspectedRouting.Typ.Type;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Expression
{
public abstract class Function : IExpression
{

View file

@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using AspectedRouting.Typ;
using Type = AspectedRouting.Typ.Type;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Expression
{
public class FunctionCall : IExpression
{
@ -34,7 +34,10 @@ namespace AspectedRouting.Functions
public object Evaluate(Context c, params IExpression[] arguments)
{
return c.GetFunction(_name).Evaluate(c, arguments);
var func = c.GetFunction(_name);
c = c.WithAspectName(_name);
return func.Evaluate(c, arguments);
}
public IExpression Specialize(IEnumerable<Type> allowedTypes)

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Linq;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Expression
{
public class ProfileMetaData
{
@ -12,19 +12,19 @@ namespace AspectedRouting.Functions
public List<string> VehicleTyps { get; }
public List<string> Metadata { get; }
public Dictionary<string, object> DefaultParameters { get; }
public Dictionary<string, Dictionary<string, object>> Profiles { get; }
public Dictionary<string, IExpression> DefaultParameters { get; }
public Dictionary<string, Dictionary<string, IExpression>> Behaviours { get; }
public IExpression Access { get; }
public IExpression Oneway { get; }
public IExpression Speed { get; }
public Dictionary<string, IExpression> Weights { get; }
public Dictionary<string, IExpression> Priority { get; }
public ProfileMetaData(string name, string description, string author, string filename,
List<string> vehicleTyps, Dictionary<string, object> defaultParameters,
Dictionary<string, Dictionary<string, object>> profiles,
List<string> vehicleTyps, Dictionary<string, IExpression> defaultParameters,
Dictionary<string, Dictionary<string, IExpression>> behaviours,
IExpression access, IExpression oneway, IExpression speed,
Dictionary<string, IExpression> weights, List<string> metadata)
Dictionary<string, IExpression> priority, List<string> metadata)
{
Name = name;
Description = description;
@ -34,16 +34,16 @@ namespace AspectedRouting.Functions
Access = access;
Oneway = oneway;
Speed = speed;
Weights = weights;
Priority = priority;
Metadata = metadata;
DefaultParameters = defaultParameters;
Profiles = profiles;
Behaviours = behaviours;
}
public override string ToString()
{
return $"Profile: {Name} {Filename}\naccess={Access}\noneway={Oneway}\nspeed={Speed}\n" +
$"weights ={string.Join(" + ", Weights.Select(kv => "#" + kv.Key + " * " + kv.Value))} ";
$"priorities = {string.Join(" + ", Priority.Select(kv => "#" + kv.Key + " * " + kv.Value))} ";
}
}
}

View file

@ -1,11 +1,13 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using Type = AspectedRouting.Typ.Type;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
// ReSharper disable UnusedMember.Global
namespace AspectedRouting.Functions
namespace AspectedRouting.Language
{
public static class Funcs
{
@ -23,12 +25,13 @@ namespace AspectedRouting.Functions
public static readonly Function ToStringFunc = new ToString();
public static readonly Function Concat = new Concat();
public static readonly Function ContainedIn = new ContainedIn();
public static readonly Function Min = new Min();
public static readonly Function Max = new Max();
public static readonly Function Sum = new Sum();
public static readonly Function Multiply = new Multiply();
public static readonly Function FirstOf = new FirstMatchOf();
public static readonly Function MustMatch = new MustMatch();

View file

@ -1,8 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class All : Function
{

View file

@ -1,7 +1,8 @@
using System.Collections.Generic;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Concat : Function
{

View file

@ -1,8 +1,9 @@
using System.Collections.Generic;
using AspectedRouting.Typ;
using Type = AspectedRouting.Typ.Type;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Const : Function
{

View file

@ -1,8 +1,9 @@
using System.Collections.Generic;
using AspectedRouting.Typ;
using Type = AspectedRouting.Typ.Type;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class ConstRight : Function
{ public override string Description { get; } =

View file

@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using Type = AspectedRouting.Typ.Type;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Constant : IExpression
{
@ -40,7 +40,7 @@ namespace AspectedRouting.Functions
}
catch (Exception e)
{
throw new Exception($"While creating a list with members {string.Join(", ", exprs)} {e.Message}", e);
throw new Exception($"While creating a list with members {string.Join(", ", exprs.Select(e => e.Optimize()))} {e.Message}", e);
}
}

View file

@ -0,0 +1,64 @@
using System.Collections.Generic;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Language.Functions
{
public class ContainedIn : Function
{
public override string Description { get; } =
"Given a list of values, checks if the argument is contained in the list.";
public override List<string> ArgNames { get; } = new List<string>{"list","a"};
public ContainedIn() : base("containedIn", true,
new[]
{
// [a] -> a -> bool
new Curry(
new ListType(new Var("a")),
new Curry(new Var("a"),
Typs.Bool))
}
)
{
}
private ContainedIn(IEnumerable<Type> types) : base("containedIn", types)
{
}
public override object Evaluate(Context c, params IExpression[] arguments)
{
var list = (IEnumerable<IExpression>) arguments[0].Evaluate(c);
var arg = arguments[1];
var result = new List<object>();
foreach (var f in list)
{
var o = f.Evaluate(c);
while (o is IExpression e)
{
o = e.Evaluate(c);
}
if (f.Equals(arg))
{
return true;
}
}
return result;
}
public override IExpression Specialize(IEnumerable<Type> allowedTypes)
{
var unified = Types.SpecializeTo(allowedTypes);
if (unified == null)
{
return null;
}
return new ContainedIn(unified);
}
}
}

View file

@ -1,8 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Default : Function
{

View file

@ -1,18 +1,19 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using Type = AspectedRouting.Typ.Type;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Dot : Function
{
public override string Description { get; } = "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 ";
public override List<string> ArgNames { get; } = new List<string>{"f","g","a"};
public static readonly Var A = new Var("fType");
public static readonly Var B = new Var("gType");
public static readonly Var C = new Var("arg");
public static readonly Var A = new Var("a");
public static readonly Var B = new Var("b");
public static readonly Var C = new Var("c");
public Dot() : base("dot", true, new[]
{
@ -34,7 +35,7 @@ namespace AspectedRouting.Functions
{
var f0 = arguments[0];
var f1 = arguments[1];
var resultType = (f1.Types.First() as Curry).ResultType;
var resultType = ((Curry) f1.Types.First()).ResultType;
var a = arguments[2];
return f0.Evaluate(c, new Constant(resultType, f1.Evaluate(c, a)));
}

View file

@ -1,12 +1,24 @@
using System;
using System.Collections.Generic;
using AspectedRouting.Typ;
using Type = AspectedRouting.Typ.Type;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class EitherFunc : Function
{
public override string Description { get; } =
"EitherFunc is a small utility function, mostly used in the parser. It allows the compiler to choose a function, based on the types.\n\n" +
"" +
"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.\n\n" +
"Disclaimer: _you should never ever need this in your profiles_";
public override List<string> ArgNames { get; } = new List<string>{"f","g","a"};
private static Var a = new Var("a");
private static Var b = new Var("b");
private static Var c = new Var("c");

View file

@ -1,7 +1,8 @@
using System.Collections.Generic;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Eq : Function
{ public override string Description { get; } = "Returns 'yes' if both values _are_ the same";

View file

@ -1,9 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using Type = AspectedRouting.Typ.Type;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class FirstMatchOf : Function
{

View file

@ -1,8 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Id : Function
{ public override string Description { get; } = "Returns the argument unchanged - the identity function. Seems useless at first sight, but useful in parsing";

View file

@ -1,7 +1,8 @@
using System.Collections.Generic;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class If : Function
{

View file

@ -1,7 +1,8 @@
using System.Collections.Generic;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Inv : Function
{

View file

@ -1,7 +1,8 @@
using System.Collections.Generic;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class ListDot : Function
{

View file

@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using Type = AspectedRouting.Typ.Type;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Mapping : Function
{
@ -78,7 +79,6 @@ namespace AspectedRouting.Functions
var otherARgs = arguments.ToList().GetRange(1, arguments.Length - 1);
if (!StringToResultFunctions.TryGetValue(key, out var resultFunction))
{
Console.WriteLine($"Warning: key {key} not found in mapping {this}");
return null;
}
@ -95,7 +95,7 @@ namespace AspectedRouting.Functions
var typeOptStr = string.Join(";", opt.Types);
var typeEStr = string.Join("; ", e.Types);
if (opt == null || !opt.Types.Any())
if (!opt.Types.Any())
{
throw new NullReferenceException($"Optimized version is null, has different or empty types: " +
$"\n{typeEStr}" +

View file

@ -1,8 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Max : Function
{

View file

@ -0,0 +1,72 @@
using System.Collections.Generic;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Language.Functions
{
public class MemberOf : Function
{
public override string Description { get; } =
"This function returns true, if the way is member of a relation matching the specified function.\n" +
"\n" +
"In order to use this for itinero 1.0, the membership _must_ be the top level expression.\n" +
"\n" +
"Conceptually, when the aspect is executed for a way, every relation will be used as argument in the subfunction `f`\n" +
"If this subfunction returns 'true', the entire aspect will return true.\n\n" +
"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.\n" +
"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.\n\n" +
"\n\n" +
"In the test.csv, one can simply use `_relation:<aspect_name>=yes` to mimic relations in your tests";
public override List<string> ArgNames { get; } = new List<string>
{
"f","tags"
};
public MemberOf() : base(
"memberOf", true,
new[]
{
new Curry(
new Curry(Typs.Tags, Typs.Bool),
new Curry(Typs.Tags, Typs.Bool))
}
)
{
}
public MemberOf(IEnumerable<Type> types) : base("memberOf", types)
{
}
public override object Evaluate(Context c, params IExpression[] arguments)
{
// var subFunction = arguments[0];
var tags =(Dictionary<string, string>) arguments[1].Evaluate(c);
var name = c.AspectName.TrimStart('$');
if(tags.TryGetValue("_relation:"+name, out var v))
{
return v;
}
return "no";
}
public override IExpression Specialize(IEnumerable<Type> allowedTypes)
{
var unified = Types.SpecializeTo(allowedTypes);
if (unified == null)
{
return null;
}
return new MemberOf(unified);
}
}
}

View file

@ -1,13 +1,16 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Min : Function
{
public override string Description { get; } = "Out of a list of values, gets the smallest value. IN case of a list of bools, this acts as `and`";
public override List<string> ArgNames { get; } = new List<string>{"list"};
public override string Description { get; } =
"Out of a list of values, gets the smallest value. IN case of a list of bools, this acts as `and`";
public override List<string> ArgNames { get; } = new List<string> {"list"};
public Min() : base("min", true,
new[]
@ -17,7 +20,6 @@ namespace AspectedRouting.Functions
new Curry(new ListType(Typs.PDouble), Typs.PDouble),
new Curry(new ListType(Typs.Double), Typs.Double),
new Curry(new ListType(Typs.Bool), Typs.Bool),
})
{
}
@ -41,11 +43,12 @@ namespace AspectedRouting.Functions
public override object Evaluate(Context c, params IExpression[] arguments)
{
var ls = ((IEnumerable<object>) arguments[0].Evaluate(c)).Where(o => o!=null);
var expectedType = (Types.First() as Curry).ResultType;
var ls = ((IEnumerable<object>) arguments[0].Evaluate(c)).Where(o => o != null);
var expectedType = ((Curry) Types.First()).ResultType;
switch (expectedType)
{ case BoolType _:
{
case BoolType _:
if (ls.Select(o => o.Equals("yes") || o.Equals("true")).All(b => b))
{
return "yes";
@ -54,9 +57,27 @@ namespace AspectedRouting.Functions
return "no";
case DoubleType _:
case PDoubleType _:
return ls.Select(o => (double) o).Min();
return ls.Select(o =>
{
while (o is IExpression e)
{
o = e.Evaluate(c);
}
return (double) o;
}).Min();
default:
return ls.Select(o => (int) o).Min();
return ls.Select(o =>
{
while (o is IExpression e)
{
o = e.Evaluate(c);
}
return (int) o;
}).Min();
}
}
}

View file

@ -1,16 +1,18 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Multiply : Function
{
public override string Description { get; } = "Multiplies all the values in a given list. On a list of booleans, this acts as 'and' or 'all'";
public override string Description { get; } =
"Multiplies all the values in a given list. On a list of booleans, this acts as 'and' or 'all'";
public override List<string> ArgNames { get; } = new List<string> {"list"};
public Multiply() : base("multiply", true,
new[]
{
@ -42,19 +44,26 @@ namespace AspectedRouting.Functions
public override object Evaluate(Context c, params IExpression[] arguments)
{
var ls = ((IEnumerable<object>) arguments[0].Evaluate(c)).Where(o => o != null);
var expectedType = (Types.First() as Curry).ResultType;
var expectedType = ((Curry) Types.First()).ResultType;
switch (expectedType)
{
case BoolType _:
foreach (var o in ls)
foreach (var oo in ls)
{
if(!(o is string s))
var o = oo;
while (o is IExpression e)
{
o = e.Evaluate(c);
}
if (!(o is string s))
{
return "no";
}
if(!(o.Equals("yes") || o.Equals("true")))
if (!(s.Equals("yes") || s.Equals("true")))
{
return "no";
}
@ -64,16 +73,28 @@ namespace AspectedRouting.Functions
case DoubleType _:
case PDoubleType _:
var mult = 1.0;
foreach (var o in ls)
foreach (var oo in ls)
{
var o = oo;
while (o is IExpression e)
{
o = e.Evaluate(c);
}
mult *= (double) o;
}
return mult;
default:
var multI = 1;
foreach (var o in ls)
foreach (var oo in ls)
{
var o = oo;
while (o is IExpression e)
{
o = e.Evaluate(c);
}
multI *= (int) o;
}

View file

@ -1,9 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using Type = AspectedRouting.Typ.Type;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class MustMatch : Function
{

View file

@ -1,18 +1,20 @@
using System.Collections.Generic;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class NotEq : Function
{
public override string Description { get; } = "Returns 'yes' if the two passed in values are _not_ the same";
public override string Description { get; } = "OVerloaded function, either boolean not or returns 'yes' if the two passed in values are _not_ the same;";
public override List<string> ArgNames { get; } = new List<string> {"a", "b"};
public NotEq() : base("notEq", true,
new[]
{
Curry.ConstructFrom(Typs.Bool, new Var("a"), new Var("a")),
Curry.ConstructFrom(Typs.String, new Var("a"), new Var("a"))
Curry.ConstructFrom(Typs.String, new Var("a"), new Var("a")),
new Curry(Typs.Bool, Typs.Bool),
})
{
Funcs.AddBuiltin(this, "not");
@ -35,6 +37,13 @@ namespace AspectedRouting.Functions
public override object Evaluate(Context c, params IExpression[] arguments)
{
if (arguments.Length == 1)
{
var booleanArg = arguments[0].Evaluate(c);
return booleanArg.Equals("no");
}
var arg0 = arguments[0].Evaluate(c);
var arg1 = arguments[1].Evaluate(c);
if ((!(arg0?.Equals(arg1) ?? false)))

View file

@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using AspectedRouting.Typ;
using Type = AspectedRouting.Typ.Type;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Parameter : IExpression
{
@ -25,16 +25,8 @@ namespace AspectedRouting.Functions
public object Evaluate(Context c, params IExpression[] args)
{
return c?.Parameters?.GetValueOrDefault(ParamName, null);
}
public void EvaluateAll(Context c, HashSet<IExpression> addTo)
{
var v = Evaluate(c);
if (v != null)
{
addTo.Add(this);
}
var paramName = ParamName.TrimStart('#'); // context saves paramnames without '#'
return c?.Parameters?.GetValueOrDefault(paramName, null);
}
public IExpression Specialize(IEnumerable<Type> allowedTypes)

View file

@ -1,8 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Parse : Function
{
@ -13,6 +14,7 @@ namespace AspectedRouting.Functions
new[]
{
new Curry(Typs.String, Typs.Double),
new Curry(Typs.String, Typs.PDouble),
})
{
}

View file

@ -1,13 +1,18 @@
using System.Collections.Generic;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
/// <summary>
/// Converts a function 'string -> string -> a' onto a function 'tags -> [a]'
/// </summary>
public class StringStringToTagsFunction : Function
{
public override string Description { get; } =
"stringToTags converts a function `string -> string -> a` into a function `tags -> [a]`";
public override List<string> ArgNames { get; } = new List<string>{"f","tags"};
private static Type baseFunction =
Curry.ConstructFrom(new Var("a"), Typs.String, Typs.String);
@ -15,7 +20,8 @@ namespace AspectedRouting.Functions
public StringStringToTagsFunction() : base("stringToTags", true,
new[]
{
new Curry(baseFunction, new Curry(Typs.Tags, new ListType(new Var("a"))))
new Curry(baseFunction,
new Curry(Typs.Tags, new ListType(new Var("a"))))
}
)
{

View file

@ -1,8 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class Sum : Function
{

View file

@ -1,7 +1,8 @@
using System.Collections.Generic;
using AspectedRouting.Typ;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions
namespace AspectedRouting.Language.Functions
{
public class ToString : Function
{

View file

@ -1,60 +1,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using AspectedRouting.Functions;
using AspectedRouting.Typ;
using Type = AspectedRouting.Typ.Type;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting
namespace AspectedRouting.Language
{
public class Context
{
public Dictionary<string, IExpression> Parameters = new Dictionary<string, IExpression>();
public Dictionary<string, AspectMetadata> DefinedFunctions = new Dictionary<string, AspectMetadata>();
public void AddParameter(string name, string value)
{
Parameters.Add(name, new Constant(value));
}
public void AddFunction(string name, AspectMetadata function)
{
if (Funcs.Builtins.ContainsKey(name))
{
throw new ArgumentException("Function " + name + " already exists, it is a builtin function");
}
if (DefinedFunctions.ContainsKey(name))
{
throw new ArgumentException("Function " + name + " already exists");
}
DefinedFunctions.Add(name, function);
}
public IExpression GetFunction(string name)
{
if (name.StartsWith("$"))
{
name = name.Substring(1);
}
if (Funcs.Builtins.ContainsKey(name))
{
return Funcs.Builtins[name];
}
if (DefinedFunctions.ContainsKey(name))
{
return DefinedFunctions[name];
}
throw new ArgumentException(
$"The function {name} is not a defined nor builtin function. Known functions are " +
string.Join(", ", DefinedFunctions.Keys));
}
}
public interface IExpression
{
IEnumerable<Type> Types { get; }
@ -79,6 +32,11 @@ namespace AspectedRouting
public static class ExpressionExtensions
{
public static object Run(this IExpression e, Context c, Dictionary<string, string> tags)
{
return e.Apply(new []{new Constant(tags)}).Evaluate(c);
}
public static IExpression Specialize(this IExpression e, Type t)
{
if (t == null)
@ -125,6 +83,7 @@ namespace AspectedRouting
/// THen specializes every expression onto this common ground
/// </summary>
/// <returns>The common ground of types</returns>
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
public static IEnumerable<IExpression> SpecializeToCommonTypes(this IEnumerable<IExpression> exprs,
out IEnumerable<Type> specializedTypes, out IEnumerable<IExpression> specializedExpressions)
{
@ -139,12 +98,13 @@ namespace AspectedRouting
}
var specialized = specializedTypes.SpecializeTo(f.Types.RenameVars(specializedTypes));
// ReSharper disable once JoinNullCheckWithUsage
if (specialized == null)
{
throw new ArgumentException("Could not unify\n "
+ "<previous items>: " + string.Join(", ", specializedTypes) +
"\nwith\n "
+ f + ": " + string.Join(", ", f.Types));
+ f.Optimize() + ": " + string.Join(", ", f.Types));
}
specializedTypes = specialized;

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ
namespace AspectedRouting.Language.Typ
{
public class BoolType : Type
{

View file

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace AspectedRouting.Typ
namespace AspectedRouting.Language.Typ
{
public class Curry : Type
{

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ
namespace AspectedRouting.Language.Typ
{
public class DoubleType : Type
{

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ
namespace AspectedRouting.Language.Typ
{
public class IntType : Type
{

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ
namespace AspectedRouting.Language.Typ
{
public class ListType : Type
{

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ
namespace AspectedRouting.Language.Typ
{
public class NatType : Type
{

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ
namespace AspectedRouting.Language.Typ
{
public class PDoubleType : Type
{

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ
namespace AspectedRouting.Language.Typ
{
public class StringType : Type
{

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ
namespace AspectedRouting.Language.Typ
{
public class TagsType : Type
{

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ
namespace AspectedRouting.Language.Typ
{
public abstract class Type
{

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Linq;
namespace AspectedRouting.Typ
namespace AspectedRouting.Language.Typ
{
public static class Typs
{

View file

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
namespace AspectedRouting.Typ
namespace AspectedRouting.Language.Typ
{
public class Var : Type
{

View file

@ -1,12 +1,7 @@
-- The different profiles
profiles = {
{
name = "comfort_safety_speed",
description = "A route which aims to be both safe and comfortable without sacrificing too much of speed",
function_name = "determine_weights_balanced",
metric = "custom"
},
{
name = "b2w",
@ -72,151 +67,10 @@ profiles = {
}
--[[
What is a relative speedup/slowdown for each key?
]]
speed_bonuses = {
access = {
dismount = 0.2
},
highway = {
path = 0.5, -- A small path through a forest is typically very slow to go through
track = 0.7 -- an unmaintained track (in Belgium: tractor path) is slower as well
},
surface = {
paved = 0.99,
asphalt = 1, -- default speed is kept
concrete = 1,
metal = 1,
wood = 1,
["concrete:lanes"] = 0.95,
["concrete:plates"] = 1,
paving_stones = 1,
sett = 0.9, -- sett slows us down with 10%
unhewn_cobblestone = 0.75,
cobblestone = 0.8,
unpaved = 0.75,
compacted = 0.99,
fine_gravel = 0.99,
gravel = 0.9,
dirt = 0.6,
earth = 0.6,
grass = 0.6,
grass_paver = 0.9,
ground = 0.7,
sand = 0.5,
woodchips = 0.5,
snow = 0.5,
pebblestone = 0.5,
mud = 0.4
},
tracktype = {
grade1 = 0.99,
grade2 = 0.8,
grade3 = 0.6,
grade4 = 0.3,
grade5 = 0.1
},
incline = {
up = 0.75,
down = 1.25,
[0] = 1,
["0%"] = 1,
["10%"] = 0.9,
["-10%"] = 1.1,
["20%"] = 0.8,
["-20%"] = 1.2,
["30%"] = 0.7,
["-30%"] = 1.3,
}
}
--[[
Determine-speed determines the average speed for the given segment based on _physical properties_,
such as surface, road incline, ...
It is _not_ concerned with a legal (or socially accepted) speed - this is capped in the calling procedure
]]
function determine_speed(attributes, result)
local factor = calculate_factor(attributes, speed_bonuses, result)
result.speed = factor * attributes.settings.default_speed
return result.speed
end
--[[
How comfortable and nice is this road?
Note: this is quite subjective as well!
Takes a guess on how _comfortable_ this road is to travel.
Here, aspects are:
- road surface and smoothness
- estimated greenery (abandoned=railway for the win)
]]
comfort_bonuses = {
highway = {
cycleway = 1.2,
primary = 0.3,
secondary = 0.4,
tertiary = 0.5,
unclassified = 0.8,
track = 0.95,
residential = 1.0,
living_street = 1.1,
footway = 0.95,
path = 0.5
},
railway = {
abandoned = 2.0
},
cycleway = {
track = 1.2
},
access = {
designated = 1.2,
dismount = 0.5
},
cyclestreet = {
yes = 1.1
},
-- Quite, but not entirely the same as in speed
surface = {
paved = 0.99,
["concrete:lanes"] = 0.8,
["concrete:plates"] = 1,
sett = 0.9, -- sett slows us down with 10%
unhewn_cobblestone = 0.75,
cobblestone = 0.8,
unpaved = 0.75,
compacted = 1.1,
fine_gravel = 0.99,
gravel = 0.9,
dirt = 0.6,
earth = 0.6,
grass = 0.6,
grass_paver = 0.9,
ground = 0.7,
sand = 0.5,
woodchips = 0.5,
snow = 0.5,
pebblestone = 0.5,
mud = 0.4
},
}
function determine_comfort(attributes, result)
return calculate_factor(attributes, comfort_bonuses, result)
end
-- Returns 1 if no access restrictions, 0 if it is permissive
function determine_permissive_score(attributes, result)
if (attributes.access == "permissive") then
@ -674,30 +528,6 @@ end
function unit_tests()
-- Speed test are with default_speed = 100, in order to efficiently determine the factor
unit_test_speed({ highway = "residential" }, 100.0)
unit_test_speed({ highway = "residential", surface = "sett" }, 90.0)
unit_test_speed({ highway = "residential", incline = "up" }, 75.0)
unit_test_speed({ highway = "residential", incline = "up", surface = "mud" }, 30.0)
unit_test_speed({ highway = "residential", incline = "down" }, 125.0)
unit_test_speed({ highway = "residential", incline = "down", surface = "sett" }, 112.5)
unit_test_comfort({ highway = "residential" }, 1.0)
unit_test_comfort({ highway = "residential", cyclestreet = "yes" }, 1.1)
unit_test_comfort({ highway = "cycleway" }, 1.2)
unit_test_comfort({ highway = "cycleway", foot = "designated" }, 1.2)
unit_test_comfort({ highway = "path", bicycle = "designated", foot = "designated" }, 0.5)
unit_test_comfort({ highway = "path", bicycle = "designated" }, 0.5)
unit_test_comfort({ highway = "footway", foot = "designated" }, 0.95)
unit_test_comfort({ highway = "primary", cycleway = "no" }, 0.3)
unit_test_comfort({ highway = "primary", cycleway = "yes" }, 0.3)
unit_test_comfort({ highway = "primary", cycleway = "track" }, 0.36)
unit_test_comfort({ highway = "secondary", cycleway = "lane" }, 0.4)
unit_test_comfort({ ["cycleway:right"] = "lane", highway = "secondary", surface = "asphalt" }, 0.4)
unit_test_comfort({ highway = "residential", surface = "asphalt", cyclestreet = "yes" }, 1.1)
unit_test_relation_tag_processor({ route = "bicycle", operator = "Stad Genk", color = "red", type = "route" },
{ StadGenk = "yes", cycle_network_colour = "red", cycle_network = "yes" });

View file

@ -0,0 +1,4 @@
access,oneway,speed,priority,highway,_relation:bicycle.network_by_operator
no,both,0,0,,
yes,both,15,1.9,residential,
yes,both,15,21.9,residential,yes
1 access oneway speed priority highway _relation:bicycle.network_by_operator
2 no both 0 0
3 yes both 15 1.9 residential
4 yes both 15 21.9 residential yes

View file

@ -1,12 +1,60 @@
{
"name": "bicycle.comfort",
"description": "Gives a comfort factor, purely based on physical aspects of the road",
"description": "Gives a comfort factor for a road, purely based on physical aspects of the road, which is a bit subjective; this takes a bit of scnery into account with a preference for `railway=abandoned` and `towpath=yes`",
"unit": "[0, 2]",
"$default": 1,
"value": {
"$multiply": {
"highway": {
"cycleway": 1.2,
"primary": 0.3,
"secondary": 0.4,
"tertiary": 0.5,
"unclassified": 0.8,
"track": 0.95,
"residential": 1.0,
"living_street": 1.1,
"footway": 0.95,
"path": 0.5
},
"railway": {
"abandoned": 2
},
"towpath": {
"yes": 2
},
"cycleway": {
"track": 1.2
},
"cyclestreet": {
"yes": 1.5
"yes": 1.1
},
"access": {
"designated": 1.2,
"dismount": 0.5
},
"surface": {
"#": "The surface mapping heavily resembles the one in speed_factor, but it is not entirely the same",
"paved": 0.99,
"concrete:lanes": 0.8,
"concrete:plates": 1.0,
"sett": 0.9,
"unhewn_cobblestone": 0.75,
"cobblestone": 0.8,
"unpaved": 0.75,
"compacted": 1.1,
"fine_gravel": 0.99,
"gravel": 0.9,
"dirt": 0.6,
"earth": 0.6,
"grass": 0.6,
"grass_paver": 0.9,
"ground": 0.7,
"sand": 0.5,
"woodchips": 0.5,
"snow": 0.5,
"pebblestone": 0.5,
"mud": 0.4
}
}
}

View file

@ -0,0 +1,18 @@
expected,highway,foot,bicycle,cyclestreet,cycleway,cycleway:right,surface,railway,towpath
1,,,,,,,,,
1,residential,,,,,,,,
1.1,residential,,,yes,,,,,
1.2,cycleway,,,,,,,,
1.2,cycleway,designated,,,,,,,
0.5,path,designated,designated,,,,,,
0.5,path,,designated,,,,,,
0.95,footway,designated,,,,,,,
0.3,primary,,,,no,,,,
0.3,primary,,,,yes,,,,
0.36,primary,,,,track,,,,
0.4,secondary,,,,lane,,,,
0.4,secondary,,,,,lane,asphalt,,
1.1,residential,,,yes,,,asphalt,,
2,,,,,,,,abandoned,
2,,,,,,,,,yes
4,,,,,,,,abandoned,yes
1 expected highway foot bicycle cyclestreet cycleway cycleway:right surface railway towpath
2 1
3 1 residential
4 1.1 residential yes
5 1.2 cycleway
6 1.2 cycleway designated
7 0.5 path designated designated
8 0.5 path designated
9 0.95 footway designated
10 0.3 primary no
11 0.3 primary yes
12 0.36 primary track
13 0.4 secondary lane
14 0.4 secondary lane asphalt
15 1.1 residential yes asphalt
16 2 abandoned
17 2 yes
18 4 abandoned yes

View file

@ -0,0 +1,4 @@
access,oneway,speed,priority,highway,_relation:bicycle.network_score
designated,both,15,22,cycleway,yes
no,,,,,
designated,both,15,2,cycleway,
1 access oneway speed priority highway _relation:bicycle.network_score
2 designated both 15 22 cycleway yes
3 no
4 designated both 15 2 cycleway

View file

@ -1,7 +1,7 @@
access,oneway,speed,weight,highway,bicycle
0,0,0,0,,
1,0,30,0.03333,cycleway,
0,0,0,0,primary,
1,0,15,0.06666,pedestrian,
1,0,15,0.0666,pedestrian,yes
1,0,30,0.033333,residential,
access,oneway,speed,priority,highway,bicycle
no,both,0,0,,
designated,both,15,15,cycleway,
no,both,0,0,primary,
dismount,both,2.25,2.25,pedestrian,
yes,both,15,15,pedestrian,yes
yes,both,15,15,residential,

1 access oneway speed weight priority highway bicycle
2 0 no 0 both 0 0 0
3 1 designated 0 both 30 15 0.03333 15 cycleway
4 0 no 0 both 0 0 0 primary
5 1 dismount 0 both 15 2.25 0.06666 2.25 pedestrian
6 1 yes 0 both 15 0.0666 15 pedestrian yes
7 1 yes 0 both 30 15 0.033333 15 residential

View file

@ -16,25 +16,27 @@
"network"
],
"defaults": {
"#max_speed": 30,
"#defaultSpeed": 15,
"#speed": 0,
"#maxspeed": 30,
"#timeNeeded": 0,
"#distance": 0,
"#comfort": 0,
"#safety": 0,
"#withDirection": "yes",
"#againstDirection": "yes",
"network": 0
"#network": 0,
"#networkOperator": []
},
"profiles": {
"behaviours": {
"fastest": {
"description": "The fastest route to your destination",
"#time_needed": 1
"#timeNeeded": 1
},
"shortest": {
"description": "The shortest route, independent of of speed",
"#distance": 1
},
"safety": {
"description": "A defensive route shying away from big roads with lots of cars",
"#safety": 1
@ -48,10 +50,12 @@
"#comfort": 1,
"#safety": 1
},
"node_networks": {
"description": "A route which prefers cycling over cycling node networks. Non-network parts prefer some comfort and safety. The on-network-part is considered flat (meaning that only distance on the node network counts)",
"brussels": {
"description": "A route preferring the cycling network by operator 'Brussels Mobility'",
"#network": 20,
"#network-node-network-only":true,
"#networkOperator": ["Brussels Mobility"],
"#comfort": 1,
"#safety": 1
}
@ -61,16 +65,20 @@
"speed": {
"$min": [
"$legal_maxspeed_be",
"#defaultSpeed"
"#maxspeed",
{
"$multiply": [
"#defaultSpeed",
"$bicycle.speed_factor"
]
}
]
},
"weight": {
"priority": {
"#comfort": "$bicycle.comfort",
"#safety": "$bicycle.safety",
"#network": "$bicycle.network_score",
"#time_needed": {
"$inv": "$speed"
},
"#network": "$bicycle.network_by_operator",
"#timeNeeded": "$speed",
"#distance": "$distance"
}
}

View file

@ -0,0 +1,13 @@
{
"name": "bicycle.network_by_operator",
"description": "The 'bicycle.network_score' returns true if the way is part of a cycling network of a certain (group of) operators",
"$memberOf": {
"$mustMatch": {
"type": "route",
"route": "bicycle",
"state": {"$notEq": "proposed"},
"operator": {"$containedIn": "#networkOperator"}
}
}
}

View file

@ -1,9 +1,12 @@
{
"name": "bicycle.network_score",
"description": "The 'bicycle.network_score' is a bit of a catch-all for bicycle networks and indicates wether or not the road is part of a matching cycling network.",
"$mustMatch": {
"type": "route",
"route": "bicycle"
"description": "The 'bicycle.network_score' returns true if the way is part of a cycling network",
"$memberOf": {
"$mustMatch": {
"type": "route",
"route": "bicycle",
"state": {"$notEq": "proposed"}
}
}
}

View file

@ -0,0 +1,62 @@
{
"name": "bicycle.speed_factor",
"description": "Calculates a speed factor for bicycles based on physical features, e.g. a sand surface will slow a cyclist down; going over pedestrian areas even more, ...",
"$multiply": {
"access": {
"#": "We have to go by foot. Default speed of 20km/h * 0.15 = 3km/h",
"dismount": 0.15
},
"highway": {
"#": "A small forest path is typically slow",
"path": 0.5,
"#": "an unmaintained track (in Belgium: tractor path) is slower as well",
"track": 0.7
},
"surface": {
"paved": 0.99,
"asphalt": 1,
"concrete": 1,
"metal": 1,
"wood": 1,
"concrete:lanes": 0.95,
"concrete:plates": 1,
"paving_stones": 1,
"sett": 0.9,
"unhewn_cobblestone": 0.75,
"cobblestone": 0.8,
"unpaved": 0.75,
"compacted": 0.99,
"fine_gravel": 0.99,
"gravel": 0.9,
"dirt": 0.6,
"earth": 0.6,
"grass": 0.6,
"grass_paver": 0.9,
"ground": 0.7,
"sand": 0.5,
"woodchips": 0.5,
"snow": 0.5,
"pebblestone": 0.5,
"mud": 0.4
},
"tracktype": {
"grade1": 0.99,
"grade2": 0.8,
"grade3": 0.6,
"grade4": 0.3,
"grade5": 0.1
},
"incline": {
"up": 0.75,
"down": 1.25,
"0": 1,
"0%": 1,
"10%": 0.9,
"-10%": 1.1,
"20%": 0.8,
"-20%": 1.2,
"30%": 0.7,
"-30%": 1.3
}
}
}

View file

@ -0,0 +1,12 @@
expected,incline,surface,highway,access
1,,,,
1,,,residential,
0.75,up,,residential,
1.25,down,,residential,
0.3,up,mud,residential,
1.125,down,sett,residential,
0.675,up,sett,residential,
0.9,,sett,residential,
1,,asphalt,residential,
0.15,,,residential,dismount
0.0315,up,mud,track,dismount
1 expected incline surface highway access
2 1
3 1 residential
4 0.75 up residential
5 1.25 down residential
6 0.3 up mud residential
7 1.125 down sett residential
8 0.675 up sett residential
9 0.9 sett residential
10 1 asphalt residential
11 0.15 residential dismount
12 0.0315 up mud track dismount

View file

@ -2,110 +2,132 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AspectedRouting.Functions;
using AspectedRouting.IO;
using AspectedRouting.IO.itinero1;
using AspectedRouting.IO.jsonParser;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Tests;
namespace AspectedRouting
{
class Program
static class Program
{
public static List<(AspectMetadata aspect, FunctionTestSuite tests)> ParseAspects(
this List<string> jsonFileNames, Context context)
{
var aspects = new List<(AspectMetadata aspect, FunctionTestSuite tests)>();
foreach (var file in jsonFileNames)
{
var fi = new FileInfo(file);
var aspect = JsonParser.AspectFromJson(context, File.ReadAllText(file), fi.Name);
if (aspect != null)
{
var testPath = fi.DirectoryName + "/" + aspect.Name + ".test.csv";
FunctionTestSuite tests = null;
if (File.Exists(testPath))
{
tests = FunctionTestSuite.FromString(aspect, File.ReadAllText(testPath));
}
aspects.Add((aspect, tests));
}
}
return aspects;
}
private static LuaPrinter GenerateLua(Context context,
List<(AspectMetadata aspect, FunctionTestSuite tests)> aspects,
ProfileMetaData profile, List<ProfileTestSuite> profileTests)
{
var luaPrinter = new LuaPrinter(context);
foreach (var (aspect, tests) in aspects)
{
luaPrinter.AddFunction(aspect);
if (tests != null)
{
luaPrinter.AddTestSuite(tests);
}
}
luaPrinter.AddProfile(profile);
foreach (var testSuite in profileTests)
{
luaPrinter.AddTestSuite(testSuite);
}
return luaPrinter;
}
private static (ProfileMetaData profile, List<ProfileTestSuite> profileTests) ParseProfile(string profilePath,
Context context)
{
var profile = JsonParser.ProfileFromJson(context, File.ReadAllText(profilePath), new FileInfo(profilePath));
profile.SanityCheckProfile(context);
var profileFi = new FileInfo(profilePath);
var profileTests = new List<ProfileTestSuite>();
foreach (var behaviourName in profile.Behaviours.Keys)
{
var testPath = profileFi.DirectoryName + "/" + profile.Name + "." + behaviourName + ".csv";
if (File.Exists(testPath))
{
var test = ProfileTestSuite.FromString(context, profile, behaviourName, File.ReadAllText(testPath));
profileTests.Add(test);
}
else
{
Console.WriteLine($"[{behaviourName}] WARNING: no test found for behaviour");
}
}
return (profile, profileTests);
}
public static void Main(string[] args)
{
MdPrinter.GenerateHelpText("IO/md/helpText.md");
var files = Directory.EnumerateFiles("Profiles", "*.json", SearchOption.AllDirectories)
.ToList();
var context = new Context();
var aspects = ParseAspects(files, context);
MdPrinter.GenerateHelpText("IO/md/helpText.md");
var testSuites = new List<FunctionTestSuite>();
var aspects = new List<AspectMetadata>();
foreach (var file in files)
foreach (var (aspect, t) in aspects)
{
var fi = new FileInfo(file);
var aspect = JsonParser.AspectFromJson(context, File.ReadAllText(file), fi.Name);
if (aspect != null)
if (t == null)
{
aspects.Add(aspect);
var testPath = fi.DirectoryName + "/" + aspect.Name + ".test.csv";
if (File.Exists(testPath))
{
var tests = FunctionTestSuite.FromString(aspect, File.ReadAllText(testPath));
testSuites.Add(tests);
}
else
{
Console.WriteLine($"[{aspect.Name}] No tests found: to directory" );
}
Console.WriteLine($"[{aspect.Name}] WARNING: no tests found: please add {aspect.Name}.test.csv");
}
else
{
t.Run();
}
}
foreach (var aspect in aspects)
{
context.AddFunction(aspect.Name, aspect);
}
var profilePath = "Profiles/bicycle/bicycle.json";
var profile = JsonParser.ProfileFromJson(context, File.ReadAllText(profilePath), new FileInfo(profilePath));
var (profile, profileTests) = ParseProfile(profilePath, context);
var profileFi = new FileInfo(profilePath);
var profileTests = new List<ProfileTestSuite>();
foreach (var profileName in profile.Profiles.Keys)
foreach (var test in profileTests)
{
var testPath = profileFi.DirectoryName + "/" + profile.Name + "." + profileName + ".csv";
if (File.Exists(testPath))
{
var test = ProfileTestSuite.FromString(profile, profileName, File.ReadAllText(testPath));
profileTests.Add(test);
}
}
profile.SanityCheckProfile();
var luaPrinter = new LuaPrinter();
luaPrinter.CreateProfile(profile, context);
var testCode = "\n\n\n\n\n\n\n\n--------------------------- Test code -------------------------\n\n\n";
foreach (var testSuite in testSuites)
{
testSuite.Run();
testCode += testSuite.ToLua() + "\n";
}
foreach (var testSuite in profileTests)
{
testCode += testSuite.ToLua() + "\n";
test.Run(context);
}
// Compatibility code, itinero-transit doesn't know 'print'
testCode += string.Join("\n",
"",
"if (itinero == nil) then",
" itinero = {}",
" itinero.log = print",
"",
" -- Itinero is not defined -> we are running from a lua interpreter -> the tests are intended",
" runTests = true",
"",
"",
"else",
" print = itinero.log",
"end",
"",
"if (not failed_tests and not failed_profile_tests) then",
" print(\"Tests OK\")",
"end"
);
var luaPrinter = GenerateLua(context, aspects, profile, profileTests);
File.WriteAllText("output.lua", luaPrinter.ToLua() + testCode);
File.WriteAllText("output.lua", luaPrinter.ToLua());
}
}
}

View file

@ -1,14 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Functions;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
namespace AspectedRouting.IO
namespace AspectedRouting.Tests
{
public class FunctionTestSuite
{
private readonly AspectMetadata _functionToApply;
private readonly IEnumerable<(string expected, Dictionary<string, string> tags)> _tests;
public readonly AspectMetadata FunctionToApply;
public readonly IEnumerable<(string expected, Dictionary<string, string> tags)> Tests;
public static FunctionTestSuite FromString(AspectMetadata function, string csvContents)
{
@ -47,47 +49,23 @@ namespace AspectedRouting.IO
AspectMetadata functionToApply,
IEnumerable<(string expected, Dictionary<string, string> tags)> tests)
{
_functionToApply = functionToApply;
_tests = tests;
}
public string ToLua()
{
var tests = string.Join("\n", _tests.Select((test, i) => ToLua(i, test.expected, test.tags)));
return "\n" + tests + "\n";
}
private string ToLua(int index, string expected, Dictionary<string, string> tags)
{
var parameters = new Dictionary<string, string>();
foreach (var (key, value) in tags)
if (functionToApply == null)
{
if (key.StartsWith("#"))
{
parameters[key.TrimStart('#')] = value;
}
throw new NullReferenceException("functionToApply is null");
}
foreach (var (paramName, _) in parameters)
{
tags.Remove("#" + paramName);
}
var funcName = _functionToApply.Name.Replace(" ", "_").Replace(".", "_");
return
$"unit_test({funcName}, \"{_functionToApply.Name}\", {index}, \"{expected}\", {parameters.ToLuaTable()}, {tags.ToLuaTable()})";
FunctionToApply = functionToApply;
Tests = tests;
}
public void Run()
{
var failed = false;
var testCase = 0;
foreach (var test in _tests)
foreach (var test in Tests)
{
testCase++;
var context = new Context();
@ -99,14 +77,14 @@ namespace AspectedRouting.IO
}
}
var actual = _functionToApply.Evaluate(context, new Constant(test.tags));
var actual = FunctionToApply.Evaluate(context, new Constant(test.tags));
if (!actual.ToString().Equals(test.expected) &&
!(actual is double actualD && Math.Abs(double.Parse(test.expected) - actualD) < 0.0001)
)
{
failed = true;
Console.WriteLine(
$"[{_functionToApply.Name}] Testcase {testCase} failed:\n Expected: {test.expected}\n actual: {actual}\n tags: {test.tags.Pretty()}");
$"[{FunctionToApply.Name}] Testcase {testCase} failed:\n Expected: {test.expected}\n actual: {actual}\n tags: {test.tags.Pretty()}");
}
}
@ -115,7 +93,7 @@ namespace AspectedRouting.IO
throw new ArgumentException("Some test failed");
}
Console.WriteLine($"[{_functionToApply.Name}] {testCase} tests successful");
Console.WriteLine($"[{FunctionToApply.Name}] {testCase} tests successful");
}
}
}

View file

@ -0,0 +1,299 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Tests
{
public struct Expected
{
public readonly string Access;
public readonly string Oneway;
public readonly double Speed;
public readonly double Weight;
public Expected(string access, string oneway, double speed, double weight)
{
Access = access;
Oneway = oneway;
Speed = speed;
Weight = weight;
}
}
public class ProfileTestSuite
{
public readonly ProfileMetaData Profile;
public readonly string BehaviourName;
public readonly IEnumerable<(Expected, Dictionary<string, string> tags)> Tests;
public static ProfileTestSuite FromString(Context c, ProfileMetaData function, string profileName,
string csvContents)
{
try
{
var all = csvContents.Split("\n").ToList();
var keys = all[0].Split(",").ToList();
keys = keys.GetRange(4, keys.Count - 4).Select(k => k.Trim()).ToList();
foreach (var k in keys)
{
if (k.StartsWith("_relations:"))
{
throw new ArgumentException(
"To inject relation memberships, use '_relation:<aspect_name>', without S after relation");
}
if (k.StartsWith("_relation:"))
{
var aspectName = k.Substring("_relation:".Length);
if (aspectName.Contains(":"))
{
throw new ArgumentException(
"To inject relation memberships, use '_relation:<aspect_name>', don't add the behaviour name");
}
if (!c.DefinedFunctions.ContainsKey(aspectName))
{
throw new ArgumentException(
$"'_relation:<aspect_name>' detected, but the aspect {aspectName} wasn't found. Try one of: " +
string.Join(",", c.DefinedFunctions.Keys));
}
}
}
var tests = new List<(Expected, Dictionary<string, string>)>();
var line = 1;
foreach (var test in all.GetRange(1, all.Count - 1))
{
line++;
if (string.IsNullOrEmpty(test.Trim()))
{
continue;
}
try
{
var testData = test.Split(",").ToList();
var speed = 0.0;
if (!string.IsNullOrEmpty(testData[2]))
{
speed = double.Parse(testData[2]);
}
var weight = 0.0;
if (!string.IsNullOrEmpty(testData[3]))
{
weight = double.Parse(testData[3]);
}
var expected = new Expected(
testData[0],
testData[1],
speed,
weight
);
var vals = testData.GetRange(4, testData.Count - 4);
var tags = new Dictionary<string, string>();
for (int i = 0; i < keys.Count; i++)
{
if (i < vals.Count && !string.IsNullOrEmpty(vals[i]))
{
tags[keys[i]] = vals[i];
}
}
tests.Add((expected, tags));
}
catch (Exception e)
{
throw new Exception("On line " + line, e);
}
}
return new ProfileTestSuite(function, profileName, tests);
}
catch (Exception e)
{
throw new Exception("In the profile test file for " + profileName, e);
}
}
public ProfileTestSuite(
ProfileMetaData profile,
string profileName,
IEnumerable<(Expected, Dictionary<string, string> tags)> tests)
{
Profile = profile;
BehaviourName = profileName;
Tests = tests;
}
private static bool Eq(Context c, string value, object result)
{
var v = Funcs.Eq.Apply(new Constant(value), new Constant(Typs.String, result));
var o = v.Evaluate(c);
return o is string s && s.Equals("yes");
}
public bool RunTest(Context c, int i, Expected expected, Dictionary<string, string> tags)
{
c = new Context(c);
tags = new Dictionary<string, string>(tags);
void Err(string message, object exp, object act)
{
Console.WriteLine(
$"[{Profile.Name}.{BehaviourName}]: Test {i} failed: {message}; expected {exp} but got {act}\n {{{tags.Pretty()}}}");
}
var success = true;
var canAccess = Profile.Access.Run(c, tags);
tags["access"] = "" + canAccess;
if (!expected.Access.Equals(canAccess))
{
Err("access value incorrect", expected.Access, canAccess);
success = false;
}
if (expected.Access.Equals("no"))
{
return success;
}
var oneway = Profile.Oneway.Run(c, tags);
tags["oneway"] = "" + oneway;
if (!Eq(c, expected.Oneway, oneway))
{
Err("oneway value incorrect", expected.Oneway, oneway);
success = false;
}
var speed = (double) Profile.Speed.Run(c, tags);
tags["speed"] = "" + speed;
c.AddFunction("speed", new AspectMetadata(new Constant(Typs.Double, speed),
"speed", "Actual speed of this function", "NA","NA","NA", true));
c.AddFunction("oneway", new AspectMetadata(new Constant(Typs.String, oneway),
"oneway", "Actual direction of this function", "NA","NA","NA", true));
c.AddFunction("access", new AspectMetadata(new Constant(Typs.String, canAccess),
"access", "Actual access of this function", "NA","NA","NA", true));
if (Math.Abs(speed - expected.Speed) > 0.0001)
{
Err("speed value incorrect", expected.Speed, speed);
success = false;
}
var priority = 0.0;
foreach (var (paramName, expression) in Profile.Priority)
{
var aspectInfluence = (double) c.Parameters[paramName].Evaluate(c);
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (aspectInfluence == 0)
{
continue;
}
var aspectWeightObj = new Apply(
Funcs.EitherFunc.Apply(Funcs.Id, Funcs.Const, expression)
, new Constant((tags))).Evaluate(c);
double aspectWeight;
switch (aspectWeightObj)
{
case bool b:
aspectWeight = b ? 1.0 : 0.0;
break;
case double d:
aspectWeight = d;
break;
case int j:
aspectWeight = j;
break;
case string s:
if (s.Equals("yes"))
{
aspectWeight = 1.0;
break;
}
else if (s.Equals("no"))
{
aspectWeight = 0.0;
break;
}
throw new Exception($"Invalid value as result for {paramName}: got string {s}");
default:
throw new Exception($"Invalid value as result for {paramName}: got object {aspectWeightObj}");
}
priority += aspectInfluence * aspectWeight;
}
if (Math.Abs(priority - expected.Weight) > 0.0001)
{
Err("weight incorrect", expected.Weight, priority);
success = false;
}
return success;
}
public void Run(Context c)
{
var parameters = new Dictionary<string, IExpression>();
foreach (var (k, v) in Profile.DefaultParameters)
{
parameters[k] = v;
}
foreach (var (k, v) in Profile.Behaviours[BehaviourName])
{
parameters[k] = v;
}
c = c.WithParameters(parameters);
var allOk = true;
var i = 1;
foreach (var (expected, tags) in Tests)
{
try
{
allOk &= RunTest(c, i, expected, tags);
}
catch (Exception e)
{
throw new Exception("In a test for " + BehaviourName, e);
}
i++;
}
if (!allOk)
{
throw new ArgumentException("Some tests failed for " + BehaviourName);
}
else
{
Console.WriteLine($"[{BehaviourName}] {Tests.Count()} tests successful");
}
}
}
}

View file

@ -19,5 +19,7 @@ namespace AspectedRouting
return factor;
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff