Compare commits

...
Sign in to create a new pull request.

12 commits

Author SHA1 Message Date
5821db3b55 Small refactoring and cleanup 2021-07-26 20:11:02 +02:00
c4cd9d1806 Refactoring 2021-07-20 14:12:06 +02:00
2cc170395c Fix typo 2021-07-20 13:09:59 +02:00
pgm-chardelv1
9aba3939c3 Adds npmignore 2021-07-20 12:31:47 +02:00
pgm-chardelv1
0dafbabc23 Changes to executable from CLI & Adds some README info 2021-07-20 11:28:22 +02:00
pgm-chardelv1
9c31a92abd Adds first version for min & max | Needs review 2021-07-19 16:12:00 +02:00
pgm-chardelv1
2ba01713ef Last commit of week 2 2021-07-15 17:00:54 +02:00
pgm-chardelv1
2289827a9d Merge branch 'feature/js-interpreter' of https://github.com/pietervdvn/AspectedRouting into feature/js-interpreter 2021-07-14 16:32:37 +02:00
pgm-chardelv1
00b6f66110 Edit gitignore 2021-07-14 16:29:09 +02:00
pgm-chardelv1
a751490e4b Add RuleSet for comfort, safety & speed factor 2021-07-14 16:28:02 +02:00
77fa88dfd4 Add extra example 2021-07-13 13:36:01 +02:00
12fc26fa44 Add stub to get started 2021-07-13 13:26:36 +02:00
13 changed files with 10258 additions and 1 deletions

11
.gitignore vendored
View file

@ -4,3 +4,14 @@
.~lock.*
output/*
AspectedRouting.sln.DotSettings
#Visual Studio Code
.vscode
# Node Modules
*/node_modules
.fake
.ionide
*/.routeExamples

View file

@ -33,7 +33,7 @@ To call a function in an aspect, one creates a hash in the JSON where exactly on
}
```
Interpreting the above expression will aways yield `no` when evaluating, as the parameters have different values. The type of the above expression is thus `Bool`.
Interpreting the above expression will always yield `no` when evaluating, as the parameters have different values. The type of the above expression is thus `Bool`.
If no key has a function invocation (thus no key starts with `$`), the hash is interpreted as a mapping:

3
javascript/.babelrc Normal file
View file

@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}

17
javascript/.npmignore Normal file
View file

@ -0,0 +1,17 @@
# Node modules
node_modules
npm_debug.log
# Git
.git
# Apple
.DS_Store
# Route Examples
.routeExamples
.tagExamples
# Visual Studio Code
.vscode

View file

@ -0,0 +1,38 @@
/**
* Example tags
*/
const tags1 = {
"highway": "residential", // 1 // Expect "yes"
"surface": "paved", // 0.99
}
const tags2 = {
"bicycle": "yes", // Expect "yes"
"cycleway": "lane",
"highway": "secondary",
"maxspeed": "50",
}
const tags3 = {
"cyclestreet": "yes",
"highway": "residential", // Expect "yes"
"maxspeed": "30",
"surface": "asphalt"
}
const tags4 = {
"highway": "track", // Expect "yes"
"surface": "asphalt",
"incline": "10%"
}
const tags5 = {
"access": "no", // Expect "no"
"bicycle": "official",
"area": "yes"
}
const tags6 = {
"surface":"dirt",
"highway":"track",
}

View file

@ -0,0 +1,220 @@
# Building a routeplanner
This document was originally written as blog post. It gives a practical, example first example to build a custom route planner.
In order to deploy:
- Build your profile by creating the relevant `.json`-files in a directory; take a peek at `Examples`
- Run the project: `cd AspectedRouting && dotnet run <inputdir> <outputdir>` (make sure the outputDirectory is _not_ a subdirectory of the input directory)
- In outputDir, you will find a bunch of lua-scripts which can be used with itinero
## A step by step example for an aspect
Let us recreate (a small part) of the legal access aspect for cyclists. The file will answer the question: __can a bicycle enter this road?__
First, we start with some metadata:
```
"name": "bicycle.legal_access",
"description": "Can a bicycle enter this road segment?",
"unit": "Yes, No",
```
The `name` field is an important one, as the aspect can be called with it in other files. The `description` and `unit`-fields however are purely as documentation - but are nonetheless important. Writing down exactly what an aspect means helps to clarify what is calculated before coding it and makes life easier down the road.
### Building the access-function
To call a function in an aspect, one creates a hash in the JSON where exactly one key starts with a `$`. The rest of the key determines which function is called, the value of the key is its first argument whereas the other keys in the hash function as other parameters. One could for example check that two values are the same with:
```
{
"$eq": "someValue",
"b": "otherValue"
}
```
Interpreting the above expression will aways yield `no` when evaluating, as the parameters have different values. The type of the above expression is thus `Bool`.
If no key has a function invocation (thus no key starts with `$`), the hash is interpreted as a mapping:
```
{
"yes": "yes"
"no": "no"
"customers": "no"
}
```
The above expression is a function of type `string -> double`. If invoked, it will convert `yes` into the value `yes` and `customers` into the value `no`. Any string not in the mapping will result in `null`.
Every expression in AspectedRouting is implicitly yet strongly typed at compile time. Having types around is cool and good for correctness, but can be constraining and the cause of boilerplate. Therefore, expressions are allowed to have _multiple_ types. Due to the context of how it is called and what the parameters of functions are, the compiler can determine exaclty which type is meant.
For example, a mapping like above can also be used to match OSM-keys:
```
{
"access": {
"yes": "no",
"no": "no",
"customers":"no"
},
"bicycle": "$id",
"construction": "no"
}
```
There is a lot to unpack here. A mapping as above is either a function taking a `string` and returning a value, or it is a function taking a `Tags`-collection and returning a collection of calculated values.
For example, passing in the collection `access=customers` in the above function will result into the value `["no"]`. Passing `access=dismount;bicycle=yes` will result in `[null, "yes"]` - the value corresponding with `access` is passed into the mapping `{"yes":"yes", "customers":"no", ...}` where no match is found resulting in `null`. The value for `bicycle` is passed into the `$id` function which simply passes back its argument.
At last, there is the cryptical `"construction":"no"`. This expression indicates that if a construction-tag is present, the resulting value should always be `no`. But how does it work exactly? When writing a constant (such as `"no"`) in an Aspected-Routing file, it is interpreted as either being the literal constant _or_ as being a function which ignores the parameter! `"no"` has thus the types `string` and `a -> string`. When used in a single mapping with type `string -> string` it is clear the first one is meant, when used in a tagsmapping with type `Tags -> string` (e.g. `{"key": "f"}`, the type of the function `f` should be `string -> b`, clearly indicating that `"no"` should be interpreted as the function which ignores the parameter. If this sounds like magic to you - don't worry about it too much. In practice, you just type what feels logical and it'll work out.
#### Combining multiple tags
The above aspect is already pretty close to a working access-calculation for cyclists - but we still have a collection of values, not a single one. We have a clear order in which we want to evaluate the tags. This too can be done with a builtin function, namely `$firstMatchOf` with the type `[string] -> (Tags -> [a]) -> (Tags -> a)`. For those not familiar with this notation for the types, this reads as: given a list of `string` and a function (which converts tags into a list of `a`), I'll give back a function that converts `Tags` into some `a`
It is used in the following way:
```
{
"$firstMatchOf":["bicycle", "construction", "access"],
"f": { ... above code ... }
}
```
At last, what if _none_ of the tags match? What do we do then? For that, there is `$default: a -> (x -> a) -> (x -> a)`. More comprehensively, this function needs a (default) value `a`, and a function calculating some `a` based on `x` and it'll give back a function that calculates an `a` based on an `x`.
Here too is an example clearer then trying to explain it:
```
{
"$default": "no",
"f": { ... above code ... }
}
```
#### Combining everything
Everything together, this gives a very basic implementation of where a cyclists can cycle! If we throw it all together, we get the following JSON file:
```
{
"name": "bicycle.legal_access",
"description": "Gives, for each type of highway, whether or not a normal bicycle can enter legally.\nNote that legal access is a bit 'grey' in the case of roads marked private and permissive, in which case these values are returned ",
"unit": "yes, no",
"$default": "no",
"f": {
"$firstMatchOf": ["bicycle", "construction", "access"],
"f": {
"access": {
"yes": "no",
"no": "no",
"customers":"no"
},
"bicycle": "$id",
"construction": "no"
}
}
}
```
It should be noted that the _actual_ implementation is more complicated then that. There are more tags to keep track of, but the above explanation should be enough to get a grasp of [legal-access-aspect for bicycles](https://github.com/pietervdvn/AspectedRouting/blob/master/Examples/bicycle/aspects/bicycle.legal_access.json). An overview of all the functions and available types, have a look [here](https://github.com/pietervdvn/AspectedRouting/blob/master/Examples/TypesAndFunctions.md)
### Building a profile
Having accessibility alone isn't enough to create a route planner for cyclists. In a similar way, one can create an aspect that defines [if the street is a oneway](https://github.com/pietervdvn/AspectedRouting/blob/master/Examples/bicycle/aspects/bicycle.oneway.json) or how [comfortable a street is](https://github.com/pietervdvn/AspectedRouting/blob/master/Examples/bicycle/aspects/bicycle.comfort.json). (Please note that the linked examples are stripped down examples. Our actual routeplanner has a few more aspect files and more tags).
At last, we have to combine those aspects into something that actually creates the profile. This is done by another JSON-file, such as [this one](https://github.com/pietervdvn/AspectedRouting/blob/master/Examples/bicycle/bicycle.json). Lets break it down:
```
{
"name": "bicycle",
"description": "Profile for a normal bicycle",
```
This is some metadata, mostly meant for humans.
```
"defaults": {
"#maxspeed": 20,
"#timeNeeded": 0,
"#comfort": 0,
"#distance": 0,
},
```
This declares some variables, which can only be used in the scope of the profile. Variables always start with `#` and are either a `number`, a `boolean` or a `string`. They are used to below the actual aspects of the profile:
```
"access": "$bicycle.legal_access",
```
This states when a segment is accessible. It expects a function `Tags -> string` and a segment is considered not accessible if this value is `"no"`; it is accessible otherwise.
```
"oneway": "$bicycle.oneway",
```
This indicates if the street is a oneway, it expects a function `Tags -> string` where the resulting value is one of `both`,`with` or `against`
```
"speed": {
"$min": [
"#defaultSpeed",
"$legal_maxspeed_be"
]
},
```
This states how fast a bicycle would be going on the segment; it expects a function `Tags -> number`. It is the first interesting case: both the variable `#maxspeed` (defined in `defaults`) is used, together with a function calculating the _legal_ max speed for a road segment. The lowest of the two is taken, by the function `$min`
```
"behaviours": {
"fastest": {
"description": "The fastest route to your destination",
"#timeNeeded": 1,
},
"shortest": {
"description": "The shortest route, independent of of speed",
"#distance": 1,
},
"comfort": {
"description": "A comfortable route preferring well-paved roads, smaller roads and a bit of scenery at the cost of speed",
"#comfort": 1
},
"electric":{
"description": "An electrical bicycle",
"#maxspeed": 25,
"#comfort":1,
"#timeNeeded": 5
},
"electric_fastest":{
"description": "An electrical bicycle, focussed on speed",
"#maxspeed": 25,
}
},
```
The above code defines _behaviours_ of the cyclist. It allows to overwrite a variable which influences the routeplanning. For example, the behavour `electrical` above will overwrite the maxspeed, changing the `speed`-aspect at the top of the file. However, these variables are most important in the priority below:
```
"priority": {
"#comfort": "$bicycle.comfort",
"#timeNeeded": "$speed",
"#distance": "$distance",
}
}
```
The priority is the core of the customizibility and calculates the priority of the segment. First, the function on the right is calculated with the tags of the segment - e.g. for a segment with tags `highway=residential;surface=sett;` this will yield `{"#comfort": 0.9, "#timeNeeded": 25, "#distance": 1}`.
These values are multiplied with the variables and summed, giving the _priority_ of the segment - where the variable are set by the requested profile; e.g. for `electrical` this will yield `(#comfort = 1) * 0.9 + (#timeNeeded = 5) * 25 + (#distance = 0) * 1`, giving the priority of `125.9`. The _cost_ per meter is then the inverted value, thus `1 / 125.9` or approximately `0.008/m`. This cost seems relatively low - but that doesn't matter as all costs are in the same range.

23
javascript/README.md Normal file
View file

@ -0,0 +1,23 @@
# StressMap interpreter
CLI program that generates a stress score based on a RuleSet JSON file and a tag object.
## Installation
`npm i mapcomplete-stressmap`
## How to use
This program can be used from the command line interface:
`node mapcomplete-stressmap [ruleset.json] [tags object]`
Example tags:
```json
{
"cyclestreet": "yes",
"highway": "residential", // Expect "yes"
"maxspeed": "30",
"surface": "asphalt"
}
```
Guidelines on JSON Ruleset (from [AspectedRouting](https://www.github.com/pietervdvn/AspectedRouting.git))
[Building a Profile](./BuildingAProfile.md)

191
javascript/RuleSet.js Normal file
View file

@ -0,0 +1,191 @@
/**
* RuleSet Class
* Constructor
* @param name {string} Name of RuleSet
* @param defaultValue {number} Default score value
* @param values {object} Main data object
*/
class RuleSet {
constructor(config) {
delete config["name"]
delete config["unit"]
delete config["description"]
this.program = config
}
/**
* getScore calculates a score for the RuleSet
* @param tags {object} Active tags to compare against
*/
runProgram(tags, program = this.program) {
if (typeof program !== "object") {
return program;
}
let functionName /*: string*/ = undefined;
let functionArguments /*: any */ = undefined
let otherValues = {}
Object.entries(program).forEach(
entry => {
const [key, value] = entry
if (key.startsWith("$")) {
functionName = key
functionArguments = value
} else {
otherValues[key] = value
}
}
)
if (functionName === undefined) {
return this.interpretAsDictionary(program, tags)
}
if (functionName === '$multiply') {
return this.multiplyScore(tags, functionArguments);
} else if (functionName === '$firstMatchOf') {
this.order = keys;
return this.getFirstMatchScore(tags);
} else if (functionName === '$min') {
return this.getMinValue(tags, functionArguments);
} else if (functionName === '$max') {
return this.getMaxValue(tags, functionArguments);
} else if (functionName === '$default') {
return this.defaultV(functionArguments, otherValues, tags)
} else {
console.error(`Error: Program ${functionName} is not implemented yet. ${JSON.stringify(program)}`);
}
}
/**
* Given a 'program' without function invocation, interprets it as a dictionary
*
* E.g., given the program
*
* {
* highway: {
* residential: 30,
* living_street: 20
* },
* surface: {
* sett : 0.9
* }
*
* }
*
* in combination with the tags {highway: residential},
*
* the result should be [30, undefined];
*
* For the tags {highway: residential, surface: sett} we should get [30, 0.9]
*
*
* @param program
* @param tags
* @return {(undefined|*)[]}
*/
interpretAsDictionary(program, tags) {
return Object.entries(tags).map(tag => {
const [key, value] = tag;
const propertyValue = program[key]
if (propertyValue === undefined) {
return undefined
}
if (typeof propertyValue !== "object") {
return propertyValue
}
return propertyValue[value]
});
}
defaultV(subProgram, otherArgs, tags) {
const normalProgram = Object.entries(otherArgs)[0][1]
const value = this.runProgram(tags, normalProgram)
if (value !== undefined) {
return value;
}
return this.runProgram(tags, subProgram)
}
/**
* Multiplies the default score with the proper values
* @param tags {object} the active tags to check against
* @param subprogram which should generate a list of values
* @returns score after multiplication
*/
multiplyScore(tags, subprogram) {
let number = 1
this.runProgram(tags, subprogram).filter(r => r !== undefined).forEach(r => number *= parseFloat(r))
return number.toFixed(2);
}
getFirstMatchScore(tags) {
let matchFound = false;
let match = "";
let i = 0;
for (let key of this.order) {
i++;
for (let entry of Object.entries(JSON.parse(tags))) {
const [tagKey, tagValue] = entry;
if (key === tagKey) {
const valueReply = this.checkValues(entry);
if (!!valueReply) {
match = valueReply;
matchFound = true;
return match;
}
}
}
}
if (!matchFound) {
match = this.defaultValue;
return match;
}
}
checkValues(tag) {
const [tagKey, tagValue] = tag;
const options = Object.entries(this.scoreValues[1])
for (let option of options) {
const [optKey, optValues] = option;
if (optKey === tagKey) {
return optValues[`${tagValue}`];
}
}
return null;
}
getMinValue(tags, subprogram) {
console.log("Running min with", tags, subprogram)
const minArr = subprogram.map(part => {
if (typeof (part) === 'object') {
const calculatedValue = this.runProgram(tags, part)
return parseFloat(calculatedValue)
} else {
return parseFloat(part);
}
}).filter(v => !isNaN(v));
return Math.min(...minArr);
}
getMaxValue(tags, subprogram) {
const maxArr = subprogram.map(part => {
if (typeof (part) === 'object') {
return parseFloat(this.runProgram(tags, part))
} else {
return parseFloat(part);
}
}).filter(v => !isNaN(v));
return Math.max(...maxArr);
}
}
export default RuleSet;

View file

@ -0,0 +1,68 @@
import { expect, jest } from '@jest/globals';
import RuleSet from '../RuleSet.js';
describe('RuleSet', () => {
const exampleJSON = {
"name": "real.name",
"$default": "1",
"value": {
"$multiply": {
"example": {
"score": "1.2"
},
"other": {
"thing": "1",
"something": "1.2",
"other_thing": "1.4"
}
},
"$firstMatchOf": [
"area",
"empty",
"things"
],
"value": {
"area": {
"yes": "no"
},
"access:": {
"no": "no",
"customers": "private"
},
"$multiply": {
"access": {
"dismount": 0.15
},
"highway": {
"path": 0.5
}
}
},
}
};
const tags = {
"example": "score",
"other": "something",
"highway": "track",
"surface": "sett",
"cycleway": "lane"
}
test('it should resolve', () => {
const ruleSet = exampleJSON;
const { name, $default, $multiply, value} = ruleSet;
if (!!value) {
const currentSet = new RuleSet(name, $default, value);
currentSet.runProgram(tags);
expect(currentSet.runProgram).resolves;
expect(currentSet.toString).resolves;
} else {
const currentSet = new RuleSet(name, $default, {$multiply});
currentSet.toString();
currentSet.runProgram(tags);
}
})
})

7
javascript/index.js Normal file
View file

@ -0,0 +1,7 @@
import interpret from './interpret.js';
import RuleSet from './RuleSet.js';
export {
interpret,
RuleSet
};

37
javascript/interpret.js Normal file
View file

@ -0,0 +1,37 @@
/**
* Import packages
*/
import fs from 'fs';
import {argv} from 'process';
import RuleSet from './RuleSet.js';
const app = {
init() {
if (!!argv && argv.length < 4) {
console.info(`Invalid command. In order to run the JavaScript interpreter please use the following format:
> node index.js [ruleset JSON file] [tags]`)
} else if (!!argv && argv.length === 4) {
const definitionFile = argv[2];
const tags = argv[3];
const result = this.interpret(definitionFile, tags);
console.log(result)
}
},
/**
* Interpret JsonFile and apply it as a tag. To use with CLI only
* @param definitionFile {any} JSON input defining the score distribution
* @param tags {any} OSM tags as key/value pairs
*/
interpret(definitionFile, tags) {
if (typeof tags === "string") {
tags = JSON.parse(tags)
}
const rawData = fs.readFileSync(definitionFile);
const program = JSON.parse(rawData);
return new RuleSet(program).runProgram(tags)
}
};
app.init();
export default app;

9616
javascript/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

26
javascript/package.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "aspected-routing",
"version": "0.2.0",
"description": "Calculates a score based on a .Json-file which contains an Aspected-Routing function. This is a (partial) javascript port",
"main": "index.js",
"type": "module",
"scripts": {
"test": "jest"
},
"jest": {
"transform": {
"^.+\\.jsx?$": "babel-jest"
}
},
"keywords": [],
"author": "Charlotte Delvaux",
"license": "MIT",
"devDependencies": {
"@babel/preset-env": "^7.14.7",
"@types/jest": "^26.0.24",
"@types/node": "^16.3.1",
"babel-jest": "^27.0.6",
"jest": "^27.0.6",
"ts-node": "^10.1.0"
}
}