Skip to content

Commit e024ace

Browse files
committed
Add code from graphhopper/graphhopper a72004c0e9a85584d43b1037edbb4e11bde84e49
1 parent d6ef7d1 commit e024ace

19 files changed

+2451
-0
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*.iml
2+
.idea
3+
node_modules
4+
dist/index.js
5+
package-lock.json

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# development
2+
3+
```shell
4+
# initially do
5+
npm install
6+
# update build whenever a file changes
7+
npm run watch
8+
# -> open demo/index.html or start live server etc.
9+
10+
# ... or keep running the tests while developing
11+
npm run test-watch
12+
```

demo/index.html

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8"/>
5+
<!-- you will need to import these css files when using the editor -->
6+
<!-- todo: add css to bundle, but currently this is not even useful, because browserify does not handle css -->
7+
<link rel="stylesheet" href="../node_modules/codemirror/lib/codemirror.css"/>
8+
<link rel="stylesheet" href="../node_modules/codemirror/addon/hint/show-hint.css"/>
9+
<link rel="stylesheet" href="../node_modules/codemirror/addon/lint/lint.css"/>
10+
</head>
11+
<body>
12+
<div id="custom_model_editor_box"></div>
13+
<button id="send-button">send</button>
14+
<script type="module">
15+
// importing this module adds a global variable GHCustomModelEditor
16+
// it would be better to be able to use es6 imports, but since we are building
17+
// with webpack this is not possible at the moment: https://github.com/webpack/webpack/issues/2933
18+
import "../dist/index.js";
19+
const categories = {
20+
"max_speed": {type: 'numeric'},
21+
"max_weight": {type: 'numeric'},
22+
"max_height": {type: 'numeric'},
23+
"max_width": {type: 'numeric'},
24+
"road_class": {type: 'enum', values: ["OTHER", "MOTORWAY", "TRUNK", "PRIMARY", "SECONDARY", "TERTIARY", "RESIDENTIAL", "UNCLASSIFIED", "SERVICE", "ROAD", "TRACK", "BRIDLEWAY", "STEPS", "CYCLEWAY", "PATH", "LIVING_STREET", "FOOTWAY", "PEDESTRIAN", "PLATFORM", "CORRIDOR"].sort()},
25+
"road_class_link": {type: 'boolean'},
26+
"road_environment": {type: 'enum', values: ["OTHER", "ROAD", "FERRY", "TUNNEL", "BRIDGE", "FORD", "SHUTTLE_TRAIN"].sort()},
27+
"road_access": {type: 'enum', values: ["YES", "DESTINATION", "CUSTOMERS", "DELIVERY", "FORESTRY", "AGRICULTURAL", "PRIVATE", "OTHER", "NO"].sort()},
28+
"surface": {type: 'enum', values: ["MISSING", "PAVED", "ASPHALT", "CONCRETE", "PAVING_STONES", "COBBLESTONE", "UNPAVED", "COMPACTED", "FINE_GRAVEL", "GRAVEL", "GROUND", "DIRT", "GRASS", "SAND", "OTHER"].sort()},
29+
"smoothness": {type: 'enum', values: ["MISSING", "EXCELLENT", "GOOD", "INTERMEDIATE", "BAD", "VERY_BAD", "HORRIBLE", "VERY_HORRIBLE", "IMPASSABLE", "OTHER"].sort()},
30+
"toll": {type: 'enum', values: ["NO", "ALL", "HGV"].sort()}
31+
};
32+
const editor = GHCustomModelEditor.create({}, (element) => {
33+
document.querySelector("#custom_model_editor_box").appendChild(element);
34+
});
35+
editor.categories = categories;
36+
editor.setExtraKey('Ctrl-Enter', () => {editor.value = (editor.value + '\n You pressed Ctrl-Enter')});
37+
editor.value = '{\n "hello": "world"\n}';
38+
editor.cm.focus();
39+
editor.cm.setCursor(editor.cm.lineCount())
40+
const button = document.querySelector("#send-button")
41+
editor.validListener = (valid) => {
42+
button.disabled = !valid;
43+
if (valid) {
44+
console.log(editor.getUsedCategories());
45+
console.log(editor.jsonObj);
46+
}
47+
}
48+
</script>
49+
</body>
50+
</html>

jest.config.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
preset: 'ts-jest/presets/js-with-ts',
3+
testEnvironment: 'node',
4+
};

package.json

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "custom-model-editor",
3+
"version": "0.0.1",
4+
"description": "Interactive text editor for GraphHopper's custom model",
5+
"main": "dist/index.js",
6+
"private": "true",
7+
"scripts": {
8+
"watch": "webpack --watch --mode development",
9+
"build-dev": "webpack --mode development",
10+
"build": "webpack --mode production",
11+
"test": "jest",
12+
"test-watch": "jest --watchAll"
13+
},
14+
"keywords": [],
15+
"author": "GraphHopper",
16+
"license": "Apache-2.0",
17+
"devDependencies": {
18+
"@types/jest": "^26.0.20",
19+
"jest": "^26.6.3",
20+
"ts-jest": "^26.5.1",
21+
"ts-loader": "^8.0.17",
22+
"typescript": "^4.1.5",
23+
"webpack": "^5.23.0",
24+
"webpack-cli": "^4.5.0"
25+
},
26+
"dependencies": {
27+
"codemirror": "^5.59.2",
28+
"jsonc-parser": "^3.0.0"
29+
}
30+
}

src/complete.js

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {parse} from './parse.js';
2+
import {tokenAtPos} from "./tokenize.js";
3+
4+
/**
5+
* Returns auto-complete suggestions for a given string/expression, categories, areas, and a character position.
6+
* The returned object contains two fields:
7+
* - suggestions: a list of suggestions/strings
8+
* - range: the character range that is supposed to be replaced by the suggestion
9+
*/
10+
function complete(expression, pos, categories, areas) {
11+
const lastNonWhitespace = getLastNonWhitespacePos(expression);
12+
if (pos > lastNonWhitespace) {
13+
// pad the expression with whitespace until pos, remove everything after pos
14+
let parseExpression = expression;
15+
while (parseExpression.length < pos)
16+
parseExpression += ' ';
17+
parseExpression = parseExpression.slice(0, pos);
18+
// we use a little trick: we run parse() on a manipulated expression where we inserted a dummy character to
19+
// see which completions are offered to us (assuming we typed in something)
20+
parseExpression += '…';
21+
const parseResult = parse(parseExpression, categories, areas);
22+
const tokenPos = tokenAtPos(parseExpression, pos);
23+
24+
// in case the expression has an error at a position that is parsed before our position we return no suggestions
25+
if (parseResult.range[0] !== tokenPos.range[0])
26+
return empty();
27+
28+
// we only keep the suggestions that match the already existing characters if there are any
29+
const suggestions = parseResult.completions.filter(c => {
30+
// we need to remove our dummy character for the filtering
31+
const partialToken = tokenPos.token.substring(0, tokenPos.token.length - 1);
32+
return startsWith(c, partialToken);
33+
});
34+
return {
35+
suggestions: suggestions,
36+
range: suggestions.length === 0 ? null : [tokenPos.range[0], pos]
37+
}
38+
} else {
39+
let tokenPos = tokenAtPos(expression, pos);
40+
// we replace the token at pos with a dummy character
41+
const parseExpression = expression.substring(0, tokenPos.range[0]) + '…' + expression.substring(tokenPos.range[1]);
42+
// pos might be a whitespace position but right at the end of the *previous* token. we have to deal with some
43+
// special cases (and this is actually similar to the situation where we are at the end of the expression).
44+
// this is quite messy, but relying on the tests for now...
45+
const modifiedTokenPos = tokenAtPos(parseExpression, tokenPos.range[0]);
46+
const parseResult = parse(parseExpression, categories, areas);
47+
if (parseResult.range[0] !== modifiedTokenPos.range[0])
48+
return empty();
49+
const suggestions = parseResult.completions.filter(c => {
50+
let partialToken = tokenPos.token === null
51+
? modifiedTokenPos.token.substring(0, modifiedTokenPos.token.length - 1)
52+
: tokenPos.token.substring(0, pos - tokenPos.range[0]);
53+
return startsWith(c, partialToken);
54+
});
55+
return {
56+
suggestions: suggestions,
57+
range: suggestions.length === 0
58+
? null
59+
: [modifiedTokenPos.range[0], tokenPos.token === null ? pos : tokenPos.range[1]]
60+
}
61+
}
62+
}
63+
64+
function empty() {
65+
return {
66+
suggestions: [],
67+
range: null
68+
}
69+
}
70+
71+
function getLastNonWhitespacePos(str) {
72+
for (let i = str.length - 1; i >= 0; --i) {
73+
if (str.slice(i, i + 1).trim() !== '') {
74+
return i;
75+
}
76+
}
77+
return -1;
78+
}
79+
80+
function startsWith(str, substr) {
81+
// str.startsWith(substr) is not supported by IE11...
82+
return str.substring(0, substr.length) === substr;
83+
}
84+
85+
export {complete};

src/complete.test.js

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import {complete} from './complete';
2+
3+
const categories = {
4+
'a': {type: 'enum', values: ['a1a', 'a1b', 'a2a', 'a2b']},
5+
'b': {type: 'enum', values: ['b1', 'b2']},
6+
'c': {type: 'numeric'}
7+
};
8+
const areas = ['pqr', 'xyz'];
9+
10+
describe("complete", () => {
11+
test("complete at end of expression", () => {
12+
test_complete('a == ', 5, ['a1a', 'a1b', 'a2a', 'a2b'], [5, 5]);
13+
test_complete('b == ', 5, ['b1', 'b2'], [5, 5]);
14+
test_complete('b == ', 5, ['b1', 'b2'], [5, 5]);
15+
test_complete('', 12, ['a', 'b', 'c', 'in_pqr', 'in_xyz', 'true', 'false'], [12, 12]);
16+
test_complete(' ', 12, ['a', 'b', 'c', 'in_pqr', 'in_xyz', 'true', 'false'], [12, 12]);
17+
test_complete('\t\n', 12, ['a', 'b', 'c', 'in_pqr', 'in_xyz', 'true', 'false'], [12, 12]);
18+
test_complete(' ', 0, ['a', 'b', 'c', 'in_pqr', 'in_xyz', 'true', 'false'], [0, 0]);
19+
test_complete(' ', 1, ['a', 'b', 'c', 'in_pqr', 'in_xyz', 'true', 'false'], [1, 1]);
20+
test_complete(' ', 2, ['a', 'b', 'c', 'in_pqr', 'in_xyz', 'true', 'false'], [2, 2]);
21+
test_complete('b == ', 4, ['b1', 'b2'], [4, 4]);
22+
test_complete('b ==', 4, ['b1', 'b2'], [4, 4]);
23+
test_complete('b ==', 9, ['b1', 'b2'], [9, 9]);
24+
test_complete('b == ', 9, ['b1', 'b2'], [9, 9]);
25+
});
26+
27+
test("complete at end of expression, incomplete token", () => {
28+
// if we complete at the end of the (non-whitespace part of the) expression we get completion suggestions
29+
// that consider the characters that were entered already
30+
test_complete('a == a1 ', 7, ['a1a', 'a1b'], [5, 7]);
31+
test_complete('a == x1 ', 7, [], null);
32+
// here we get no suggestions, because the 'previous' expression is invalid (we evaluate 'a == a1 x')
33+
test_complete('a == a1 ', 8, [], null);
34+
});
35+
36+
test("complete at end of expression, with previous error", () => {
37+
// in case the expression contains an error that is found before the position our cursor is at we do not
38+
// get any suggestions, because without further work they are simply not available! we do *not* want to get
39+
// suggestions to fix the first error (here a/b instead of xyz)!
40+
test_complete('xyz == a1 && a == ', 20, [], null);
41+
// here we get suggestions despite the error (unmatched opening '('). this is because even though the opening
42+
// parentheses comes first in the expression the error is not detected before the position we are editing is
43+
// inspected
44+
test_complete('( a == ', 8, ['a1a', 'a1b', 'a2a', 'a2b'], [8, 8]);
45+
});
46+
47+
test("complete at whitespace within expression", () => {
48+
test_complete('a == a1 && b == b1', 1, ['a'], [0, 1]);
49+
test_complete('a == a1 && b == b1', 4, [], null);
50+
test_complete('a == a1 && b == b1', 5, ['a1a', 'a1b', 'a2a', 'a2b'], [5, 7]);
51+
test_complete('a == a1 && b == b1', 7, ['a1a', 'a1b'], [5, 7]);
52+
test_complete('a == x1a && b == b1', 5, ['a1a', 'a1b', 'a2a', 'a2b'], [5, 8]);
53+
// no completions when there is a previous error
54+
test_complete('a == x1a && b == b1', 8, [], null);
55+
});
56+
57+
test("complete at token within expression", () => {
58+
test_complete('a == a1a && b != b1', 0, ['a', 'b', 'c', 'in_pqr', 'in_xyz', 'true', 'false'], [0, 1]);
59+
test_complete('a == a1a && b != b2', 2, ['==', '!='], [2, 4]);
60+
test_complete('a == a1b && b == b1', 5, ['a1a', 'a1b', 'a2a', 'a2b'], [5, 8]);
61+
test_complete('a == a2a && b == b2', 6, ['a1a', 'a1b', 'a2a', 'a2b'], [5, 8]);
62+
test_complete('a == x2b && b == b1 || a == a1a', 5, ['a1a', 'a1b', 'a2a', 'a2b'], [5, 8]);
63+
// here we filter out some of the options due to the current cursor position
64+
test_complete('a == a2b && b == b1', 7, ['a2a', 'a2b'], [5, 8]);
65+
test_complete('a == a2b && b == b1', 8, ['a2b'], [5, 8]);
66+
test_complete('a == a && b == b1', 5, ['a1a', 'a1b', 'a2a', 'a2b'], [5, 6]);
67+
test_complete('a == a && b == b1', 6, ['a1a', 'a1b', 'a2a', 'a2b'], [5, 6]);
68+
test_complete('a == a2 && b == b1', 6, ['a1a', 'a1b', 'a2a', 'a2b'], [5, 7]);
69+
test_complete('a == a2 && b == b1', 7, ['a2a', 'a2b'], [5, 7]);
70+
// no completions when there is a previous error
71+
test_complete('a == x2b && b == b1 || a == a1a', 12, [], null);
72+
test_complete('a == x2b && b == b1 || a == a1a', 17, [], null);
73+
});
74+
75+
test("complete areas", () => {
76+
test_complete('in_ && a == a1a', 2, ['in_pqr', 'in_xyz'], [0, 3]);
77+
test_complete('in_x && a == a1a', 4, ['in_xyz'], [0, 4]);
78+
});
79+
80+
test("complete update", () => {
81+
test_complete(`c < 9`, 4, [`__hint__type a number`], [4, 5]);
82+
// 'continuing' a number does not work because the __hint__ suggestion is filtered when it is compared with
83+
// the existing digits. should we change this?
84+
test_complete(`c < 9 || b == b1`, 5, [], null);
85+
test_complete(`c < 9 `, 5, [], null);
86+
test_complete(`c < 9`, 5, [], null);
87+
});
88+
});
89+
90+
function test_complete(expression, pos, suggestions, range) {
91+
const completion = complete(expression, pos, categories, areas)
92+
try {
93+
expect(completion.suggestions).toStrictEqual(suggestions);
94+
expect(completion.range).toStrictEqual(range);
95+
} catch (e) {
96+
Error.captureStackTrace(e, test_complete);
97+
throw e;
98+
}
99+
}

0 commit comments

Comments
 (0)