-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
convert.js
244 lines (214 loc) · 8.45 KB
/
convert.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
// @flow
import assert from 'assert';
import type {StylePropertySpecification} from '../style-spec';
export default convertFunction;
function convertFunction(parameters: any, propertySpec: StylePropertySpecification) {
let stops = parameters.stops;
if (!stops) {
// identity function
return convertIdentityFunction(parameters, propertySpec);
}
const zoomAndFeatureDependent = stops && typeof stops[0][0] === 'object';
const featureDependent = zoomAndFeatureDependent || parameters.property !== undefined;
const zoomDependent = zoomAndFeatureDependent || !featureDependent;
stops = stops.map((stop) => {
if (!featureDependent && propertySpec.tokens && typeof stop[1] === 'string') {
return [stop[0], convertTokenString(stop[1])];
}
return [stop[0], ['literal', stop[1]]];
});
if (zoomAndFeatureDependent) {
return convertZoomAndPropertyFunction(parameters, propertySpec, stops);
} else if (zoomDependent) {
return convertZoomFunction(parameters, propertySpec, stops);
} else {
return convertPropertyFunction(parameters, propertySpec, stops);
}
}
function convertIdentityFunction(parameters, propertySpec): Array<mixed> {
const get = ['get', parameters.property];
if (parameters.default === undefined) {
// By default, expressions for string-valued properties get coerced. To preserve
// legacy function semantics, insert an explicit assertion instead.
return propertySpec.type === 'string' ? ['string', get] : get;
} else if (propertySpec.type === 'enum') {
return [
'match',
get,
Object.keys(propertySpec.values),
get,
parameters.default
];
} else {
const expression = [propertySpec.type === 'color' ? 'to-color' : propertySpec.type, get, ['literal', parameters.default]];
if (propertySpec.type === 'array') {
expression.splice(1, 0, propertySpec.value, propertySpec.length || null);
}
return expression;
}
}
function getInterpolateOperator(parameters) {
switch (parameters.colorSpace) {
case 'hcl': return 'interpolate-hcl';
case 'lab': return 'interpolate-lab';
default: return 'interpolate';
}
}
function convertZoomAndPropertyFunction(parameters, propertySpec, stops) {
const featureFunctionParameters = {};
const featureFunctionStops = {};
const zoomStops = [];
for (let s = 0; s < stops.length; s++) {
const stop = stops[s];
const zoom = stop[0].zoom;
if (featureFunctionParameters[zoom] === undefined) {
featureFunctionParameters[zoom] = {
zoom: zoom,
type: parameters.type,
property: parameters.property,
default: parameters.default,
};
featureFunctionStops[zoom] = [];
zoomStops.push(zoom);
}
featureFunctionStops[zoom].push([stop[0].value, stop[1]]);
}
// the interpolation type for the zoom dimension of a zoom-and-property
// function is determined directly from the style property specification
// for which it's being used: linear for interpolatable properties, step
// otherwise.
const functionType = getFunctionType({}, propertySpec);
if (functionType === 'exponential') {
const expression = [getInterpolateOperator(parameters), ['linear'], ['zoom']];
for (const z of zoomStops) {
const output = convertPropertyFunction(featureFunctionParameters[z], propertySpec, featureFunctionStops[z]);
appendStopPair(expression, z, output, false);
}
return expression;
} else {
const expression = ['step', ['zoom']];
for (const z of zoomStops) {
const output = convertPropertyFunction(featureFunctionParameters[z], propertySpec, featureFunctionStops[z]);
appendStopPair(expression, z, output, true);
}
fixupDegenerateStepCurve(expression);
return expression;
}
}
function coalesce(a, b) {
if (a !== undefined) return a;
if (b !== undefined) return b;
}
function convertPropertyFunction(parameters, propertySpec, stops) {
const type = getFunctionType(parameters, propertySpec);
const get = ['get', parameters.property];
if (type === 'categorical' && typeof stops[0][0] === 'boolean') {
assert(parameters.stops.length > 0 && parameters.stops.length <= 2);
const expression = ['case'];
for (const stop of stops) {
expression.push(['==', get, stop[0]], stop[1]);
}
expression.push(['literal', coalesce(parameters.default, propertySpec.default)]);
return expression;
} else if (type === 'categorical') {
const expression = ['match', get];
for (const stop of stops) {
appendStopPair(expression, stop[0], stop[1], false);
}
expression.push(['literal', coalesce(parameters.default, propertySpec.default)]);
return expression;
} else if (type === 'interval') {
const expression = ['step', ['number', get]];
for (const stop of stops) {
appendStopPair(expression, stop[0], stop[1], true);
}
fixupDegenerateStepCurve(expression);
return parameters.default === undefined ? expression : [
'case',
['==', ['typeof', get], 'number'],
expression,
['literal', parameters.default]
];
} else if (type === 'exponential') {
const base = parameters.base !== undefined ? parameters.base : 1;
const expression = [getInterpolateOperator(parameters), ['exponential', base], ['number', get]];
for (const stop of stops) {
appendStopPair(expression, stop[0], stop[1], false);
}
return parameters.default === undefined ? expression : [
'case',
['==', ['typeof', get], 'number'],
expression,
['literal', parameters.default]
];
} else {
throw new Error(`Unknown property function type ${type}`);
}
}
function convertZoomFunction(parameters, propertySpec, stops, input = ['zoom']) {
const type = getFunctionType(parameters, propertySpec);
let expression;
let isStep = false;
if (type === 'interval') {
expression = ['step', input];
isStep = true;
} else if (type === 'exponential') {
const base = parameters.base !== undefined ? parameters.base : 1;
expression = [getInterpolateOperator(parameters), ['exponential', base], input];
} else {
throw new Error(`Unknown zoom function type "${type}"`);
}
for (const stop of stops) {
appendStopPair(expression, stop[0], stop[1], isStep);
}
fixupDegenerateStepCurve(expression);
return expression;
}
function fixupDegenerateStepCurve(expression) {
// degenerate step curve (i.e. a constant function): add a noop stop
if (expression[0] === 'step' && expression.length === 3) {
expression.push(0);
expression.push(expression[3]);
}
}
function appendStopPair(curve, input, output, isStep) {
// Skip duplicate stop values. They were not validated for functions, but they are for expressions.
// https://github.com/mapbox/mapbox-gl-js/issues/4107
if (curve.length > 3 && input === curve[curve.length - 2]) {
return;
}
// step curves don't get the first input value, as it is redundant.
if (!(isStep && curve.length === 2)) {
curve.push(input);
}
curve.push(output);
}
function getFunctionType(parameters, propertySpec) {
if (parameters.type) {
return parameters.type;
} else {
assert(propertySpec.expression);
return (propertySpec.expression: any).interpolated ? 'exponential' : 'interval';
}
}
// "String with {name} token" => ["concat", "String with ", ["get", "name"], " token"]
export function convertTokenString(s: string) {
const result = ['concat'];
const re = /{([^{}]+)}/g;
let pos = 0;
for (let match = re.exec(s); match !== null; match = re.exec(s)) {
const literal = s.slice(pos, re.lastIndex - match[0].length);
pos = re.lastIndex;
if (literal.length > 0) result.push(literal);
result.push(['to-string', ['get', match[1]]]);
}
if (result.length === 1) {
return s;
}
if (pos < s.length) {
result.push(s.slice(pos));
} else if (result.length === 2) {
return result[1];
}
return result;
}