vega2ol 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/main.d.ts +25 -0
- package/dist/main.js +238 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/main.d.ts +25 -0
- package/dist/src/main.js +240 -0
- package/dist/tests/test-basic.d.ts +1 -0
- package/dist/tests/test-basic.js +165 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# vega2ol
|
|
2
|
+
|
|
3
|
+
A TypeScript library to transpile [Vega expressions](https://vega.github.io/vega/docs/expressions/) to [OpenLayers](https://openlayers.org/) expressions.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`vega2ol` automatically converts Vega expression syntax to OpenLayers array-based expression format. This enables seamless integration between Vega-Lite/Vega visualization specifications and OpenLayers map styling.
|
|
8
|
+
|
|
9
|
+
### Example
|
|
10
|
+
|
|
11
|
+
**Vega Expression:**
|
|
12
|
+
```javascript
|
|
13
|
+
datum.value > 100 ? 'red' : 'blue'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**OpenLayers Expression:**
|
|
17
|
+
```javascript
|
|
18
|
+
['case', ['>', ['get', 'value'], 100], 'red', 'blue']
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install vega2ol
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { vega2ol } from 'vega2ol';
|
|
31
|
+
|
|
32
|
+
// Simple conditional styling
|
|
33
|
+
const olExpr = vega2ol("datum.population > 1000000 ? '#FF0000' : '#0000FF'");
|
|
34
|
+
// Returns: ['case', ['>', ['get', 'population'], 1000000], '#FF0000', '#0000FF']
|
|
35
|
+
|
|
36
|
+
// Use in OpenLayers style
|
|
37
|
+
const style = {
|
|
38
|
+
'fill-color': olExpr
|
|
39
|
+
};
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Supported Features
|
|
43
|
+
|
|
44
|
+
### Literals
|
|
45
|
+
- Numbers: `42`, `3.14`, `-10`
|
|
46
|
+
- Strings: `'hello'`, `"world"`
|
|
47
|
+
- Booleans: `true`, `false`
|
|
48
|
+
- Null: `null`
|
|
49
|
+
|
|
50
|
+
### Member Access
|
|
51
|
+
- `datum.fieldName` ā `['get', 'fieldName']`
|
|
52
|
+
- Works with any data field
|
|
53
|
+
|
|
54
|
+
### Operators
|
|
55
|
+
|
|
56
|
+
**Comparison:**
|
|
57
|
+
- `==`, `!=`, `<`, `<=`, `>`, `>=`
|
|
58
|
+
|
|
59
|
+
**Logical:**
|
|
60
|
+
- `&&` (AND) ā maps to `all`
|
|
61
|
+
- `||` (OR) ā maps to `any`
|
|
62
|
+
- `!` (NOT) ā maps to `!`
|
|
63
|
+
|
|
64
|
+
**Arithmetic:**
|
|
65
|
+
- `+`, `-`, `*`, `/`, `%`
|
|
66
|
+
|
|
67
|
+
### Conditional Expressions
|
|
68
|
+
- Ternary operator: `condition ? true_value : false_value`
|
|
69
|
+
- Nested ternaries supported for multiple conditions
|
|
70
|
+
|
|
71
|
+
### Functions
|
|
72
|
+
|
|
73
|
+
**Math Functions:**
|
|
74
|
+
- `floor()`, `ceil()`, `round()`, `abs()`, `sqrt()`
|
|
75
|
+
- `pow()`, `log()`, `exp()`, `sin()`, `cos()`, `tan()`
|
|
76
|
+
- `min()`, `max()`
|
|
77
|
+
|
|
78
|
+
**String Functions:**
|
|
79
|
+
- `tostring()`, `upper()`, `lower()`, `substring()`
|
|
80
|
+
|
|
81
|
+
**Array Functions:**
|
|
82
|
+
- `length()`, `indexof()`, `slice()`
|
|
83
|
+
|
|
84
|
+
**Type Functions:**
|
|
85
|
+
- `type()`, `isvalid()`, `isnumber()`, `isstring()`, `isboolean()`
|
|
86
|
+
|
|
87
|
+
### Vega Constants
|
|
88
|
+
- Math: `PI`, `E`, `LN2`, `LN10`, `LOG2E`, `LOG10E`, `SQRT1_2`, `SQRT2`
|
|
89
|
+
- Number: `MIN_VALUE`, `MAX_VALUE`
|
|
90
|
+
|
|
91
|
+
## Development
|
|
92
|
+
|
|
93
|
+
### Setup
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Install dependencies
|
|
97
|
+
npm install
|
|
98
|
+
|
|
99
|
+
# Build
|
|
100
|
+
npm run build
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Testing
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Run tests
|
|
107
|
+
npm test
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Architecture
|
|
111
|
+
|
|
112
|
+
The transpiler uses a **visitor pattern** similar to AST walkers:
|
|
113
|
+
|
|
114
|
+
1. **Parser**: Uses `vega-expression` to parse Vega expressions into an Abstract Syntax Tree (AST)
|
|
115
|
+
2. **Visitor**: `VegaToOLVisitor` class walks the AST and converts each node type to OpenLayers format
|
|
116
|
+
3. **Output**: Returns OpenLayers expression array
|
|
117
|
+
|
|
118
|
+
## Examples
|
|
119
|
+
|
|
120
|
+
### Color Mapping Based on Value
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const vegaExpr = `
|
|
124
|
+
datum.value > 100 ? 'red' :
|
|
125
|
+
datum.value > 50 ? 'yellow' :
|
|
126
|
+
'green'
|
|
127
|
+
`;
|
|
128
|
+
const olExpr = vega2ol(vegaExpr);
|
|
129
|
+
// Use with OpenLayers style
|
|
130
|
+
style['fill-color'] = olExpr;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Opacity Based on Category
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const vegaExpr = "datum.category == 'important' ? 1 : 0.5";
|
|
137
|
+
const olExpr = vega2ol(vegaExpr);
|
|
138
|
+
style['fill-opacity'] = olExpr;
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Complex Conditions
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const vegaExpr = `
|
|
145
|
+
(datum.x > 0 && datum.y < 100) ? 'inside' : 'outside'
|
|
146
|
+
`;
|
|
147
|
+
const olExpr = vega2ol(vegaExpr);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Resources
|
|
151
|
+
|
|
152
|
+
- [Vega Expression Documentation](https://vega.github.io/vega/docs/expressions/)
|
|
153
|
+
- [Vega Expression Parser](https://github.com/vega/vega/tree/main/packages/vega-expression)
|
|
154
|
+
- [OpenLayers Documentation](https://openlayers.org/)
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenLayers expression types
|
|
3
|
+
* OL expressions are arrays: ['operator', arg1, arg2, ...] or ['case', condition, value, ...]
|
|
4
|
+
*/
|
|
5
|
+
type OLExpression = any[] | string | number | boolean | null;
|
|
6
|
+
/**
|
|
7
|
+
* Vega AST node types (simplified)
|
|
8
|
+
*/
|
|
9
|
+
interface VegaNode {
|
|
10
|
+
type: string;
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Custom error class for Vega2OL transpilation errors
|
|
15
|
+
*/
|
|
16
|
+
export declare class Vega2OLError extends Error {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Main transpiler function
|
|
21
|
+
* @param vegaExpressionStr - A Vega expression string (e.g., "datum.value > 100 ? 'red' : 'blue'")
|
|
22
|
+
* @returns OpenLayers expression array
|
|
23
|
+
*/
|
|
24
|
+
export declare function vega2ol(vegaExpressionStr: string): Promise<OLExpression>;
|
|
25
|
+
export { OLExpression, VegaNode };
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator mapping from Vega operators to OpenLayers operators
|
|
3
|
+
*/
|
|
4
|
+
const OPERATOR_MAPPING = {
|
|
5
|
+
// Comparison operators
|
|
6
|
+
"==": "==",
|
|
7
|
+
"!=": "!=",
|
|
8
|
+
"<": "<",
|
|
9
|
+
"<=": "<=",
|
|
10
|
+
">": ">",
|
|
11
|
+
">=": ">=",
|
|
12
|
+
// Logical operators
|
|
13
|
+
"&&": "all",
|
|
14
|
+
"||": "any",
|
|
15
|
+
// Arithmetic operators
|
|
16
|
+
"+": "+",
|
|
17
|
+
"-": "-",
|
|
18
|
+
"*": "*",
|
|
19
|
+
"/": "/",
|
|
20
|
+
"%": "%",
|
|
21
|
+
// Other
|
|
22
|
+
"**": "pow",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Function mapping from Vega functions to OpenLayers functions
|
|
26
|
+
*/
|
|
27
|
+
const FUNCTION_MAPPING = {
|
|
28
|
+
// Math functions
|
|
29
|
+
abs: "abs",
|
|
30
|
+
floor: "floor",
|
|
31
|
+
ceil: "ceil",
|
|
32
|
+
round: "round",
|
|
33
|
+
sqrt: "sqrt",
|
|
34
|
+
pow: "pow",
|
|
35
|
+
log: "log",
|
|
36
|
+
exp: "exp",
|
|
37
|
+
sin: "sin",
|
|
38
|
+
cos: "cos",
|
|
39
|
+
tan: "tan",
|
|
40
|
+
// String functions
|
|
41
|
+
tostring: "toString",
|
|
42
|
+
upper: "toUpperCase",
|
|
43
|
+
lower: "toLowerCase",
|
|
44
|
+
substring: "substring",
|
|
45
|
+
// Array functions
|
|
46
|
+
length: "length",
|
|
47
|
+
indexof: "indexOf",
|
|
48
|
+
slice: "slice",
|
|
49
|
+
// Type functions
|
|
50
|
+
type: "type",
|
|
51
|
+
isvalid: "isValid",
|
|
52
|
+
isnumber: "isNumber",
|
|
53
|
+
isstring: "isString",
|
|
54
|
+
isboolean: "isBoolean",
|
|
55
|
+
// Aggregation functions
|
|
56
|
+
min: "min",
|
|
57
|
+
max: "max",
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Custom error class for Vega2OL transpilation errors
|
|
61
|
+
*/
|
|
62
|
+
export class Vega2OLError extends Error {
|
|
63
|
+
constructor(message) {
|
|
64
|
+
const fullMessage = `${message}, note that only a subset of Vega is supported`;
|
|
65
|
+
super(fullMessage);
|
|
66
|
+
this.name = "Vega2OLError";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Visitor class to convert Vega AST to OpenLayers expressions
|
|
71
|
+
*/
|
|
72
|
+
class VegaToOLVisitor {
|
|
73
|
+
constructor() {
|
|
74
|
+
this.scope = {};
|
|
75
|
+
}
|
|
76
|
+
visit(node) {
|
|
77
|
+
if (!node || !node.type) {
|
|
78
|
+
throw new Vega2OLError("Invalid AST node");
|
|
79
|
+
}
|
|
80
|
+
const method = `visit${node.type}`;
|
|
81
|
+
if (typeof this[method] === "function") {
|
|
82
|
+
return this[method](node);
|
|
83
|
+
}
|
|
84
|
+
throw new Vega2OLError(`Unsupported ${node.type} node`);
|
|
85
|
+
}
|
|
86
|
+
// Literal values
|
|
87
|
+
visitLiteral(node) {
|
|
88
|
+
const { value } = node;
|
|
89
|
+
if (value === null)
|
|
90
|
+
return null;
|
|
91
|
+
if (typeof value === "boolean")
|
|
92
|
+
return value;
|
|
93
|
+
if (typeof value === "number")
|
|
94
|
+
return value;
|
|
95
|
+
if (typeof value === "string")
|
|
96
|
+
return value;
|
|
97
|
+
throw new Vega2OLError(`Unsupported literal type: ${typeof value}`);
|
|
98
|
+
}
|
|
99
|
+
// Member access: datum.value, datum['value']
|
|
100
|
+
visitMemberExpression(node) {
|
|
101
|
+
const { object, property } = node;
|
|
102
|
+
if (object.type === "Identifier" && object.name === "datum") {
|
|
103
|
+
if (typeof property === "string") {
|
|
104
|
+
return ["get", property];
|
|
105
|
+
}
|
|
106
|
+
// For computed properties - if property is an Identifier with 'name'
|
|
107
|
+
if (property.type === "Identifier" && property.name) {
|
|
108
|
+
return ["get", property.name];
|
|
109
|
+
}
|
|
110
|
+
// For computed properties like datum[variable]
|
|
111
|
+
return ["get", this.visit(property)];
|
|
112
|
+
}
|
|
113
|
+
throw new Vega2OLError(`Unsupported member access`);
|
|
114
|
+
}
|
|
115
|
+
// Binary operations: a + b, a > b, etc.
|
|
116
|
+
visitBinaryExpression(node) {
|
|
117
|
+
const { operator, left, right } = node;
|
|
118
|
+
const leftOL = this.visit(left);
|
|
119
|
+
const rightOL = this.visit(right);
|
|
120
|
+
// Handle special cases
|
|
121
|
+
if (operator === "in") {
|
|
122
|
+
return ["in", leftOL, rightOL];
|
|
123
|
+
}
|
|
124
|
+
if (operator === "&&") {
|
|
125
|
+
return ["all", leftOL, rightOL];
|
|
126
|
+
}
|
|
127
|
+
if (operator === "||") {
|
|
128
|
+
return ["any", leftOL, rightOL];
|
|
129
|
+
}
|
|
130
|
+
const olOp = OPERATOR_MAPPING[operator];
|
|
131
|
+
if (!olOp) {
|
|
132
|
+
throw new Vega2OLError(`Unsupported binary operator: ${operator}`);
|
|
133
|
+
}
|
|
134
|
+
return [olOp, leftOL, rightOL];
|
|
135
|
+
}
|
|
136
|
+
// Unary operations: !x, -x, +x
|
|
137
|
+
visitUnaryExpression(node) {
|
|
138
|
+
const { operator, argument } = node;
|
|
139
|
+
const argOL = this.visit(argument);
|
|
140
|
+
if (operator === "!") {
|
|
141
|
+
return ["!", argOL];
|
|
142
|
+
}
|
|
143
|
+
if (operator === "-") {
|
|
144
|
+
return ["*", argOL, -1];
|
|
145
|
+
}
|
|
146
|
+
if (operator === "+") {
|
|
147
|
+
return argOL;
|
|
148
|
+
}
|
|
149
|
+
throw new Vega2OLError(`Unsupported unary operator: ${operator}`);
|
|
150
|
+
}
|
|
151
|
+
// Conditional: test ? consequent : alternate
|
|
152
|
+
visitConditionalExpression(node) {
|
|
153
|
+
const { test, consequent, alternate } = node;
|
|
154
|
+
const testOL = this.visit(test);
|
|
155
|
+
const consequentOL = this.visit(consequent);
|
|
156
|
+
const alternateOL = this.visit(alternate);
|
|
157
|
+
// OL 'case' expression: ['case', condition1, value1, condition2, value2, ..., defaultValue]
|
|
158
|
+
return ["case", testOL, consequentOL, alternateOL];
|
|
159
|
+
}
|
|
160
|
+
// Function calls: floor(x), ceil(y), etc.
|
|
161
|
+
visitCallExpression(node) {
|
|
162
|
+
const { callee, arguments: args } = node;
|
|
163
|
+
let funcName;
|
|
164
|
+
if (callee.type === "Identifier") {
|
|
165
|
+
funcName = callee.name;
|
|
166
|
+
}
|
|
167
|
+
else if (callee.type === "MemberExpression" &&
|
|
168
|
+
callee.object.name === "Math") {
|
|
169
|
+
funcName = callee.property;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
throw new Vega2OLError(`Unsupported function call`);
|
|
173
|
+
}
|
|
174
|
+
const olFunc = FUNCTION_MAPPING[funcName.toLowerCase()];
|
|
175
|
+
if (!olFunc) {
|
|
176
|
+
throw new Vega2OLError(`Unsupported function: ${funcName}`);
|
|
177
|
+
}
|
|
178
|
+
const olArgs = args.map((arg) => this.visit(arg));
|
|
179
|
+
return [olFunc, ...olArgs];
|
|
180
|
+
}
|
|
181
|
+
// Array literal: [1, 2, 3]
|
|
182
|
+
visitArrayExpression(node) {
|
|
183
|
+
const { elements } = node;
|
|
184
|
+
return elements.map((elem) => this.visit(elem));
|
|
185
|
+
}
|
|
186
|
+
// Identifier/Variable: x, PI, etc.
|
|
187
|
+
visitIdentifier(node) {
|
|
188
|
+
const { name } = node;
|
|
189
|
+
// Check if it's in the current scope
|
|
190
|
+
if (name in this.scope) {
|
|
191
|
+
return this.scope[name];
|
|
192
|
+
}
|
|
193
|
+
// Vega constants
|
|
194
|
+
const vegaConstants = {
|
|
195
|
+
PI: Math.PI,
|
|
196
|
+
E: Math.E,
|
|
197
|
+
LN2: Math.LN2,
|
|
198
|
+
LN10: Math.LN10,
|
|
199
|
+
LOG2E: Math.LOG2E,
|
|
200
|
+
LOG10E: Math.LOG10E,
|
|
201
|
+
SQRT1_2: Math.SQRT1_2,
|
|
202
|
+
SQRT2: Math.SQRT2,
|
|
203
|
+
MIN_VALUE: Number.MIN_VALUE,
|
|
204
|
+
MAX_VALUE: Number.MAX_VALUE,
|
|
205
|
+
};
|
|
206
|
+
if (name in vegaConstants) {
|
|
207
|
+
return vegaConstants[name];
|
|
208
|
+
}
|
|
209
|
+
// Return as string (might be a function or field)
|
|
210
|
+
return name;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Main transpiler function
|
|
215
|
+
* @param vegaExpressionStr - A Vega expression string (e.g., "datum.value > 100 ? 'red' : 'blue'")
|
|
216
|
+
* @returns OpenLayers expression array
|
|
217
|
+
*/
|
|
218
|
+
export async function vega2ol(vegaExpressionStr) {
|
|
219
|
+
if (typeof vegaExpressionStr !== "string") {
|
|
220
|
+
throw new Vega2OLError("Input must be a string");
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
// Dynamically import parseExpression to handle ESM
|
|
224
|
+
const { parseExpression } = await import("vega-expression");
|
|
225
|
+
// Parse Vega expression to AST
|
|
226
|
+
const ast = parseExpression(vegaExpressionStr);
|
|
227
|
+
// Visit the AST and convert to OL expression
|
|
228
|
+
const visitor = new VegaToOLVisitor();
|
|
229
|
+
const olExpression = visitor.visit(ast);
|
|
230
|
+
return olExpression;
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (error instanceof Vega2OLError) {
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
throw new Vega2OLError(`Failed to parse expression: ${error.message}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ExpressionValue } from "ol/expr/expression.js";
|
|
2
|
+
/**
|
|
3
|
+
* OpenLayers expression types
|
|
4
|
+
* OL expressions are arrays: ['operator', arg1, arg2, ...] or ['case', condition, value, ...]
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Vega AST node types (simplified)
|
|
8
|
+
*/
|
|
9
|
+
interface VegaNode {
|
|
10
|
+
type: string;
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Custom error class for Vega2OL transpilation errors
|
|
15
|
+
*/
|
|
16
|
+
export declare class Vega2OLError extends Error {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Main transpiler function
|
|
21
|
+
* @param vegaExpressionStr - A Vega expression string (e.g., "datum.value > 100 ? 'red' : 'blue'")
|
|
22
|
+
* @returns OpenLayers expression array
|
|
23
|
+
*/
|
|
24
|
+
export declare function vega2ol(vegaExpressionStr: string): Promise<ExpressionValue>;
|
|
25
|
+
export { ExpressionValue as OLExpression, VegaNode };
|
package/dist/src/main.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { parseExpression } from "vega-expression";
|
|
2
|
+
/**
|
|
3
|
+
* Operator mapping from Vega operators to OpenLayers operators
|
|
4
|
+
*/
|
|
5
|
+
const OPERATOR_MAPPING = {
|
|
6
|
+
// Comparison operators
|
|
7
|
+
"==": "==",
|
|
8
|
+
"!=": "!=",
|
|
9
|
+
"<": "<",
|
|
10
|
+
"<=": "<=",
|
|
11
|
+
">": ">",
|
|
12
|
+
">=": ">=",
|
|
13
|
+
// Logical operators
|
|
14
|
+
"&&": "all",
|
|
15
|
+
"||": "any",
|
|
16
|
+
// Arithmetic operators
|
|
17
|
+
"+": "+",
|
|
18
|
+
"-": "-",
|
|
19
|
+
"*": "*",
|
|
20
|
+
"/": "/",
|
|
21
|
+
"%": "%",
|
|
22
|
+
// Other
|
|
23
|
+
"**": "pow",
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Constants mapping from Vega constants to OpenLayers constants
|
|
27
|
+
*/
|
|
28
|
+
const CONSTANTS_MAPPING = {
|
|
29
|
+
PI: Math.PI,
|
|
30
|
+
E: Math.E,
|
|
31
|
+
LN2: Math.LN2,
|
|
32
|
+
LN10: Math.LN10,
|
|
33
|
+
LOG2E: Math.LOG2E,
|
|
34
|
+
LOG10E: Math.LOG10E,
|
|
35
|
+
SQRT1_2: Math.SQRT1_2,
|
|
36
|
+
SQRT2: Math.SQRT2,
|
|
37
|
+
MIN_VALUE: Number.MIN_VALUE,
|
|
38
|
+
MAX_VALUE: Number.MAX_VALUE,
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Function mapping from Vega functions to OpenLayers functions
|
|
42
|
+
*/
|
|
43
|
+
const FUNCTION_MAPPING = {
|
|
44
|
+
// Math functions
|
|
45
|
+
abs: "abs",
|
|
46
|
+
floor: "floor",
|
|
47
|
+
ceil: "ceil",
|
|
48
|
+
round: "round",
|
|
49
|
+
sqrt: "sqrt",
|
|
50
|
+
pow: "pow",
|
|
51
|
+
log: "log",
|
|
52
|
+
exp: "exp",
|
|
53
|
+
sin: "sin",
|
|
54
|
+
cos: "cos",
|
|
55
|
+
tan: "tan",
|
|
56
|
+
// String functions
|
|
57
|
+
tostring: "toString",
|
|
58
|
+
upper: "toUpperCase",
|
|
59
|
+
lower: "toLowerCase",
|
|
60
|
+
substring: "substring",
|
|
61
|
+
// Array functions
|
|
62
|
+
length: "length",
|
|
63
|
+
indexof: "indexOf",
|
|
64
|
+
slice: "slice",
|
|
65
|
+
// Type functions
|
|
66
|
+
type: "type",
|
|
67
|
+
isvalid: "isValid",
|
|
68
|
+
isnumber: "isNumber",
|
|
69
|
+
isstring: "isString",
|
|
70
|
+
isboolean: "isBoolean",
|
|
71
|
+
// Aggregation functions
|
|
72
|
+
min: "min",
|
|
73
|
+
max: "max",
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Custom error class for Vega2OL transpilation errors
|
|
77
|
+
*/
|
|
78
|
+
export class Vega2OLError extends Error {
|
|
79
|
+
constructor(message) {
|
|
80
|
+
const fullMessage = `${message}, note that only a subset of Vega is supported`;
|
|
81
|
+
super(fullMessage);
|
|
82
|
+
this.name = "Vega2OLError";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Visitor class to convert Vega AST to OpenLayers expressions
|
|
87
|
+
*/
|
|
88
|
+
class VegaToOLVisitor {
|
|
89
|
+
constructor() {
|
|
90
|
+
this.scope = {};
|
|
91
|
+
}
|
|
92
|
+
visit(node) {
|
|
93
|
+
if (!node || !node.type) {
|
|
94
|
+
throw new Vega2OLError("Invalid AST node");
|
|
95
|
+
}
|
|
96
|
+
const method = `visit${node.type}`;
|
|
97
|
+
if (typeof this[method] === "function") {
|
|
98
|
+
return this[method](node);
|
|
99
|
+
}
|
|
100
|
+
throw new Vega2OLError(`Unsupported ${node.type} node`);
|
|
101
|
+
}
|
|
102
|
+
// Literal values
|
|
103
|
+
visitLiteral(node) {
|
|
104
|
+
const { value } = node;
|
|
105
|
+
if (value === null)
|
|
106
|
+
return null;
|
|
107
|
+
if (typeof value === "boolean")
|
|
108
|
+
return value;
|
|
109
|
+
if (typeof value === "number")
|
|
110
|
+
return value;
|
|
111
|
+
if (typeof value === "string")
|
|
112
|
+
return value;
|
|
113
|
+
throw new Vega2OLError(`Unsupported literal type: ${typeof value}`);
|
|
114
|
+
}
|
|
115
|
+
// Member access: datum.value, datum['value']
|
|
116
|
+
visitMemberExpression(node) {
|
|
117
|
+
const { object, property } = node;
|
|
118
|
+
if (object.type === "Identifier" && object.name === "datum") {
|
|
119
|
+
if (typeof property === "string") {
|
|
120
|
+
return ["get", property];
|
|
121
|
+
}
|
|
122
|
+
// For computed properties - if property is an Identifier with 'name'
|
|
123
|
+
if (property.type === "Identifier" && property.name) {
|
|
124
|
+
return ["get", property.name];
|
|
125
|
+
}
|
|
126
|
+
// For computed properties like datum[variable]
|
|
127
|
+
return ["get", this.visit(property)];
|
|
128
|
+
}
|
|
129
|
+
throw new Vega2OLError(`Unsupported member access`);
|
|
130
|
+
}
|
|
131
|
+
// Binary operations: a + b, a > b, etc.
|
|
132
|
+
visitBinaryExpression(node) {
|
|
133
|
+
const { operator, left, right } = node;
|
|
134
|
+
const leftOL = this.visit(left);
|
|
135
|
+
const rightOL = this.visit(right);
|
|
136
|
+
const olOp = OPERATOR_MAPPING[operator];
|
|
137
|
+
if (!olOp) {
|
|
138
|
+
throw new Vega2OLError(`Unsupported binary operator: ${operator}`);
|
|
139
|
+
}
|
|
140
|
+
return [olOp, leftOL, rightOL];
|
|
141
|
+
}
|
|
142
|
+
// Unary operations: !x, -x, +x
|
|
143
|
+
visitUnaryExpression(node) {
|
|
144
|
+
const { operator, argument } = node;
|
|
145
|
+
const argOL = this.visit(argument);
|
|
146
|
+
if (operator === "!") {
|
|
147
|
+
return ["!", argOL];
|
|
148
|
+
}
|
|
149
|
+
if (operator === "-") {
|
|
150
|
+
return ["*", argOL, -1];
|
|
151
|
+
}
|
|
152
|
+
if (operator === "+") {
|
|
153
|
+
return argOL;
|
|
154
|
+
}
|
|
155
|
+
throw new Vega2OLError(`Unsupported unary operator: ${operator}`);
|
|
156
|
+
}
|
|
157
|
+
// Conditional: test ? consequent : alternate
|
|
158
|
+
visitConditionalExpression(node) {
|
|
159
|
+
const { test, consequent, alternate } = node;
|
|
160
|
+
const testOL = this.visit(test);
|
|
161
|
+
const consequentOL = this.visit(consequent);
|
|
162
|
+
const alternateOL = this.visit(alternate);
|
|
163
|
+
// OL 'case' expression: ['case', condition1, value1, condition2, value2, ..., defaultValue]
|
|
164
|
+
return ["case", testOL, consequentOL, alternateOL];
|
|
165
|
+
}
|
|
166
|
+
// Function calls: floor(x), ceil(y), etc.
|
|
167
|
+
visitCallExpression(node) {
|
|
168
|
+
const { callee, arguments: args } = node;
|
|
169
|
+
let funcName;
|
|
170
|
+
if (callee.type === "Identifier") {
|
|
171
|
+
funcName = callee.name;
|
|
172
|
+
}
|
|
173
|
+
else if (callee.type === "MemberExpression" &&
|
|
174
|
+
callee.object.name === "Math") {
|
|
175
|
+
funcName = callee.property;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
throw new Vega2OLError(`Unsupported function call`);
|
|
179
|
+
}
|
|
180
|
+
const olFunc = FUNCTION_MAPPING[funcName.toLowerCase()];
|
|
181
|
+
if (!olFunc) {
|
|
182
|
+
throw new Vega2OLError(`Unsupported function: ${funcName}`);
|
|
183
|
+
}
|
|
184
|
+
const olArgs = args.map((arg) => this.visit(arg));
|
|
185
|
+
return [olFunc, ...olArgs];
|
|
186
|
+
}
|
|
187
|
+
// Array literal: [1, 2, 3]
|
|
188
|
+
visitArrayExpression(node) {
|
|
189
|
+
const { elements } = node;
|
|
190
|
+
return elements.map((elem) => this.visit(elem));
|
|
191
|
+
}
|
|
192
|
+
// Logical expressions
|
|
193
|
+
visitLogicalExpression(node) {
|
|
194
|
+
const { operator, left, right } = node;
|
|
195
|
+
const leftOL = this.visit(left);
|
|
196
|
+
const rightOL = this.visit(right);
|
|
197
|
+
const olOp = OPERATOR_MAPPING[operator];
|
|
198
|
+
if (!olOp) {
|
|
199
|
+
throw new Vega2OLError(`Unsupported logical operator: ${operator}`);
|
|
200
|
+
}
|
|
201
|
+
return [olOp, leftOL, rightOL];
|
|
202
|
+
}
|
|
203
|
+
// Identifier/Variable: x, PI, etc.
|
|
204
|
+
visitIdentifier(node) {
|
|
205
|
+
const { name } = node;
|
|
206
|
+
// Check if it's in the current scope
|
|
207
|
+
if (name in this.scope) {
|
|
208
|
+
return this.scope[name];
|
|
209
|
+
}
|
|
210
|
+
if (name in CONSTANTS_MAPPING) {
|
|
211
|
+
return CONSTANTS_MAPPING[name];
|
|
212
|
+
}
|
|
213
|
+
// Return as string (might be a function or field)
|
|
214
|
+
return name;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Main transpiler function
|
|
219
|
+
* @param vegaExpressionStr - A Vega expression string (e.g., "datum.value > 100 ? 'red' : 'blue'")
|
|
220
|
+
* @returns OpenLayers expression array
|
|
221
|
+
*/
|
|
222
|
+
export async function vega2ol(vegaExpressionStr) {
|
|
223
|
+
if (typeof vegaExpressionStr !== "string") {
|
|
224
|
+
throw new Vega2OLError("Input must be a string");
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
// Parse Vega expression to AST
|
|
228
|
+
const ast = parseExpression(vegaExpressionStr);
|
|
229
|
+
// Visit the AST and convert to OL expression
|
|
230
|
+
const visitor = new VegaToOLVisitor();
|
|
231
|
+
const olExpression = visitor.visit(ast);
|
|
232
|
+
return olExpression;
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (error instanceof Vega2OLError) {
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
throw new Vega2OLError(`Failed to parse expression: ${error.message}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { vega2ol, Vega2OLError } from "../src/main.js";
|
|
2
|
+
// Simple assertion helper
|
|
3
|
+
function assertEqual(actual, expected, message) {
|
|
4
|
+
const actualStr = JSON.stringify(actual);
|
|
5
|
+
const expectedStr = JSON.stringify(expected);
|
|
6
|
+
if (actualStr === expectedStr) {
|
|
7
|
+
console.log(` ā
${message}`);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
console.log(` ā ${message}`);
|
|
12
|
+
console.log(` Expected: ${expectedStr}`);
|
|
13
|
+
console.log(` Got: ${actualStr}`);
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Run tests
|
|
18
|
+
async function runTests() {
|
|
19
|
+
console.log("š Testing vega2ol transpiler\n");
|
|
20
|
+
let passed = 0;
|
|
21
|
+
let failed = 0;
|
|
22
|
+
try {
|
|
23
|
+
// Test 1: Literal values
|
|
24
|
+
console.log("š Literal Values");
|
|
25
|
+
if (assertEqual(await vega2ol("42"), 42, "Number literal"))
|
|
26
|
+
passed++;
|
|
27
|
+
else
|
|
28
|
+
failed++;
|
|
29
|
+
if (assertEqual(await vega2ol("'hello'"), "hello", "String literal"))
|
|
30
|
+
passed++;
|
|
31
|
+
else
|
|
32
|
+
failed++;
|
|
33
|
+
if (assertEqual(await vega2ol("true"), true, "Boolean literal"))
|
|
34
|
+
passed++;
|
|
35
|
+
else
|
|
36
|
+
failed++;
|
|
37
|
+
// Test 2: Member access
|
|
38
|
+
console.log("\nš Member Access (datum.field)");
|
|
39
|
+
if (assertEqual(await vega2ol("datum.value"), ["get", "value"], "Simple datum access"))
|
|
40
|
+
passed++;
|
|
41
|
+
else
|
|
42
|
+
failed++;
|
|
43
|
+
if (assertEqual(await vega2ol("datum.population"), ["get", "population"], "Datum field access"))
|
|
44
|
+
passed++;
|
|
45
|
+
else
|
|
46
|
+
failed++;
|
|
47
|
+
// Test 3: Comparison operators
|
|
48
|
+
console.log("\nš Comparison Operators");
|
|
49
|
+
if (assertEqual(await vega2ol("datum.value > 100"), [">", ["get", "value"], 100], "Greater than operator"))
|
|
50
|
+
passed++;
|
|
51
|
+
else
|
|
52
|
+
failed++;
|
|
53
|
+
if (assertEqual(await vega2ol("datum.value == 100"), ["==", ["get", "value"], 100], "Equality operator"))
|
|
54
|
+
passed++;
|
|
55
|
+
else
|
|
56
|
+
failed++;
|
|
57
|
+
// Test 4: Ternary/Conditional (the main example from docs)
|
|
58
|
+
console.log("\nš Conditional Expressions");
|
|
59
|
+
if (assertEqual(await vega2ol("datum.value > 100 ? 'red' : 'blue'"), ["case", [">", ["get", "value"], 100], "red", "blue"], "Simple ternary expression"))
|
|
60
|
+
passed++;
|
|
61
|
+
else
|
|
62
|
+
failed++;
|
|
63
|
+
// Test 5: Complex nested conditional
|
|
64
|
+
if (assertEqual(await vega2ol("datum.value > 100 ? 'red' : datum.value > 50 ? 'yellow' : 'green'"), [
|
|
65
|
+
"case",
|
|
66
|
+
[">", ["get", "value"], 100],
|
|
67
|
+
"red",
|
|
68
|
+
["case", [">", ["get", "value"], 50], "yellow", "green"],
|
|
69
|
+
], "Nested ternary expression"))
|
|
70
|
+
passed++;
|
|
71
|
+
else
|
|
72
|
+
failed++;
|
|
73
|
+
// Test 6: Logical operators
|
|
74
|
+
console.log("\nš Logical Operators");
|
|
75
|
+
if (assertEqual(await vega2ol("datum.x > 0 && datum.y < 100"), ["all", [">", ["get", "x"], 0], ["<", ["get", "y"], 100]], "AND operator"))
|
|
76
|
+
passed++;
|
|
77
|
+
else
|
|
78
|
+
failed++;
|
|
79
|
+
if (assertEqual(await vega2ol("datum.a > 1 || datum.b < 5"), ["any", [">", ["get", "a"], 1], ["<", ["get", "b"], 5]], "OR operator"))
|
|
80
|
+
passed++;
|
|
81
|
+
else
|
|
82
|
+
failed++;
|
|
83
|
+
// Test 7: Arithmetic operators
|
|
84
|
+
console.log("\nš Arithmetic Operators");
|
|
85
|
+
if (assertEqual(await vega2ol("datum.a + datum.b"), ["+", ["get", "a"], ["get", "b"]], "Addition operator"))
|
|
86
|
+
passed++;
|
|
87
|
+
else
|
|
88
|
+
failed++;
|
|
89
|
+
if (assertEqual(await vega2ol("datum.a * datum.b"), ["*", ["get", "a"], ["get", "b"]], "Multiplication operator"))
|
|
90
|
+
passed++;
|
|
91
|
+
else
|
|
92
|
+
failed++;
|
|
93
|
+
// Test 8: Unary operations
|
|
94
|
+
console.log("\nš Unary Operations");
|
|
95
|
+
if (assertEqual(await vega2ol("!datum.active"), ["!", ["get", "active"]], "Negation operator"))
|
|
96
|
+
passed++;
|
|
97
|
+
else
|
|
98
|
+
failed++;
|
|
99
|
+
if (assertEqual(await vega2ol("-datum.value"), ["*", ["get", "value"], -1], "Unary minus"))
|
|
100
|
+
passed++;
|
|
101
|
+
else
|
|
102
|
+
failed++;
|
|
103
|
+
// Test 9: Function calls
|
|
104
|
+
console.log("\nš Function Calls");
|
|
105
|
+
if (assertEqual(await vega2ol("floor(datum.value)"), ["floor", ["get", "value"]], "Floor function"))
|
|
106
|
+
passed++;
|
|
107
|
+
else
|
|
108
|
+
failed++;
|
|
109
|
+
if (assertEqual(await vega2ol("pow(datum.base, 2)"), ["pow", ["get", "base"], 2], "Pow function with multiple args"))
|
|
110
|
+
passed++;
|
|
111
|
+
else
|
|
112
|
+
failed++;
|
|
113
|
+
// Test 10: Real-world example - Color mapping
|
|
114
|
+
console.log("\nš Real-world Examples");
|
|
115
|
+
if (assertEqual(await vega2ol("datum.population > 1000000 ? '#FF0000' : datum.population > 500000 ? '#FFA500' : '#FFFF00'"), [
|
|
116
|
+
"case",
|
|
117
|
+
[">", ["get", "population"], 1000000],
|
|
118
|
+
"#FF0000",
|
|
119
|
+
["case", [">", ["get", "population"], 500000], "#FFA500", "#FFFF00"],
|
|
120
|
+
], "Color mapping by population size"))
|
|
121
|
+
passed++;
|
|
122
|
+
else
|
|
123
|
+
failed++;
|
|
124
|
+
// Test 11: Array expressions
|
|
125
|
+
console.log("\nš Array Expressions");
|
|
126
|
+
if (assertEqual(await vega2ol("[1, 2, 3]"), [1, 2, 3], "Array literal"))
|
|
127
|
+
passed++;
|
|
128
|
+
else
|
|
129
|
+
failed++;
|
|
130
|
+
// Test 12: Constants
|
|
131
|
+
console.log("\nš Vega Constants");
|
|
132
|
+
if (assertEqual(await vega2ol("PI"), Math.PI, "PI constant"))
|
|
133
|
+
passed++;
|
|
134
|
+
else
|
|
135
|
+
failed++;
|
|
136
|
+
// Test 13: Error handling
|
|
137
|
+
console.log("\nš Error Handling");
|
|
138
|
+
try {
|
|
139
|
+
await vega2ol(null);
|
|
140
|
+
console.log("ā Should throw for null input");
|
|
141
|
+
failed++;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof Vega2OLError) {
|
|
145
|
+
console.log(" ā
Throws Vega2OLError for invalid input");
|
|
146
|
+
passed++;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
console.log("ā Wrong error type");
|
|
150
|
+
failed++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.error("š„ Test suite error:", error);
|
|
156
|
+
failed++;
|
|
157
|
+
}
|
|
158
|
+
// Summary
|
|
159
|
+
console.log("\n" + "=".repeat(50));
|
|
160
|
+
console.log(`š Test Results: ${passed} passed, ${failed} failed`);
|
|
161
|
+
console.log("=".repeat(50));
|
|
162
|
+
return failed === 0 ? 0 : 1;
|
|
163
|
+
}
|
|
164
|
+
// Run the tests
|
|
165
|
+
runTests().then((code) => process.exit(code));
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vega2ol",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Vega expression to Open-Layers expression transpiler",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"vega",
|
|
7
|
+
"openlayers",
|
|
8
|
+
"expression",
|
|
9
|
+
"transpiler",
|
|
10
|
+
"geospatial"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/nakul-py/vega2ol#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/nakul-py/vega2ol/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/nakul-py/vega2ol.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "BSD-3-Clause",
|
|
21
|
+
"author": "nakul",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"types": "dist/index.d.ts",
|
|
25
|
+
"directories": {
|
|
26
|
+
"test": "tests"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"test": "node dist/tests/test-basic.js",
|
|
34
|
+
"dev": "tsc --watch"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"vega-expression": "^6.1.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^20.19.39",
|
|
41
|
+
"ol": "^9.2.4",
|
|
42
|
+
"ts-node": "^10.9.2",
|
|
43
|
+
"typescript": "^5.9.3"
|
|
44
|
+
}
|
|
45
|
+
}
|