Skip to content

Commit

Permalink
Merge pull request #14837 from jahow/add-get-type-operator
Browse files Browse the repository at this point in the history
Expressions / add `geometry-type` operator, fix exponential `interpolate`
  • Loading branch information
jahow committed Jun 17, 2023
2 parents f3a5cd0 + b4bc8b7 commit b80fc19
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 15 deletions.
18 changes: 14 additions & 4 deletions examples/webgl-points-layer.js
Expand Up @@ -13,7 +13,7 @@ const vectorSource = new Vector({
});

const predefinedStyles = {
'icons': {
icons: {
symbol: {
symbolType: 'image',
src: 'data/icon.png',
Expand All @@ -23,7 +23,7 @@ const predefinedStyles = {
offset: [0, 9],
},
},
'triangles': {
triangles: {
symbol: {
symbolType: 'triangle',
size: 18,
Expand Down Expand Up @@ -68,7 +68,7 @@ const predefinedStyles = {
opacity: 0.95,
},
},
'circles': {
circles: {
symbol: {
symbolType: 'circle',
size: [
Expand Down Expand Up @@ -97,7 +97,17 @@ const predefinedStyles = {
'circles-zoom': {
symbol: {
symbolType: 'circle',
size: ['interpolate', ['exponential', 2.5], ['zoom'], 2, 1, 14, 32],
// by using an exponential interpolation with a base of 2 we can make it so that circles will have a fixed size
// in world coordinates between zoom level 5 and 15
size: [
'interpolate',
['exponential', 2],
['zoom'],
5,
3,
15,
3 * Math.pow(2, 10),
],
color: ['match', ['get', 'hover'], 1, '#ff3f3f', '#006688'],
offset: [0, 0],
opacity: 0.95,
Expand Down
54 changes: 53 additions & 1 deletion src/ol/style/expressions.js
Expand Up @@ -24,6 +24,10 @@ import {asArray, fromString, isStringColor} from '../color.js';
* * `['get', 'attributeName', typeHint]` fetches a feature property value, similar to `feature.get('attributeName')`
* A type hint can optionally be specified, in case the resulting expression contains a type ambiguity which
* will make it invalid. Type hints can be one of: 'string', 'color', 'number', 'boolean', 'number[]'
* * `['geometry-type']` returns a feature's geometry type as string, either: 'LineString', 'Point' or 'Polygon'
* `Multi*` values are returned as their singular equivalent
* `Circle` geometries are returned as 'Polygon'
* `GeometryCollection` geometries are returned as the type of the first geometry found in the collection
* * `['resolution']` returns the current resolution
* * `['time']` returns the time in seconds since the creation of the layer
* * `['var', 'varName']` fetches a value from the style variables; will throw an error if that variable is undefined
Expand Down Expand Up @@ -246,6 +250,8 @@ function printTypes(valueType) {
* @typedef {Object} ParsingContextExternal
* @property {string} name Name, unprefixed
* @property {ValueTypes} type One of the value types constants
* @property {function(import("../Feature.js").FeatureLike): *} [callback] Function used for computing the attribute value;
* if undefined, `feature.get(attribute.name)` will be used
*/

/**
Expand Down Expand Up @@ -696,6 +702,46 @@ Operators['resolution'] = {
},
};

Operators['geometry-type'] = {
getReturnType: function () {
return ValueTypes.STRING;
},
toGlsl: function (context, args) {
assertArgsCount(args, 0);
const name = 'geometryType';
const computeType = (geometry) => {
const type = geometry.getType();
switch (type) {
case 'Point':
case 'LineString':
case 'Polygon':
return type;
case 'MultiPoint':
case 'MultiLineString':
case 'MultiPolygon':
return type.substring(5);
case 'Circle':
return 'Polygon';
case 'GeometryCollection':
return computeType(geometry.getGeometries()[0]);
default:
}
};
const existing = context.attributes.find((a) => a.name === name);
if (!existing) {
context.attributes.push({
name: name,
type: ValueTypes.STRING,
callback: (feature) => {
return computeType(feature.getGeometry());
},
});
}
const prefix = context.inFragmentShader ? 'v_' : 'a_';
return prefix + name;
},
};

Operators['*'] = {
getReturnType: function (args) {
let outputType = ValueTypes.NUMBER | ValueTypes.COLOR;
Expand Down Expand Up @@ -1121,7 +1167,13 @@ Operators['interpolate'] = {
result || expressionToGlsl(context, args[i + 1], outputType);
const stop2 = expressionToGlsl(context, args[i + 2], inputType);
const output2 = expressionToGlsl(context, args[i + 3], outputType);
result = `mix(${output1}, ${output2}, pow(clamp((${input} - ${stop1}) / (${stop2} - ${stop1}), 0.0, 1.0), ${exponent}))`;
let ratio;
if (interpolation === 1) {
ratio = `(${input} - ${stop1}) / (${stop2} - ${stop1})`;
} else {
ratio = `(pow(${exponent}, (${input} - ${stop1})) - 1.0) / (pow(${exponent}, (${stop2} - ${stop1})) - 1.0)`;
}
result = `mix(${output1}, ${output2}, clamp(${ratio}, 0.0, 1.0))`;
}
return result;
},
Expand Down
4 changes: 3 additions & 1 deletion src/ol/webgl/styleparser.js
Expand Up @@ -370,7 +370,9 @@ export function parseLiteralStyle(style) {

const attributes = vertContext.attributes.map(function (attribute) {
let callback;
if (attribute.type === ValueTypes.STRING) {
if (attribute.callback) {
callback = attribute.callback;
} else if (attribute.type === ValueTypes.STRING) {
callback = (feature) =>
getStringNumberEquivalent(vertContext, feature.get(attribute.name));
} else if (attribute.type === ValueTypes.COLOR) {
Expand Down
76 changes: 71 additions & 5 deletions test/browser/spec/ol/style/expressions.test.js
@@ -1,3 +1,12 @@
import Feature from '../../../../../src/ol/Feature.js';
import {
Circle,
GeometryCollection,
MultiLineString,
MultiPoint,
MultiPolygon,
Point,
} from '../../../../../src/ol/geom.js';
import {
ValueTypes,
arrayToGlsl,
Expand Down Expand Up @@ -911,7 +920,7 @@ describe('ol/style/expressions', function () {
ValueTypes.ANY
)
).to.eql(
'mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 1.0, 0.0, 1.0), pow(clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0), 1.0))'
'mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 1.0, 0.0, 1.0), clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0))'
);
expect(
expressionToGlsl(
Expand All @@ -930,7 +939,7 @@ describe('ol/style/expressions', function () {
ValueTypes.ANY
)
).to.eql(
'mix(mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 1.0, 0.0, 1.0), pow(clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0), 1.0)), vec4(0.0, 0.0, 1.0, 1.0), pow(clamp((a_attr - 2000.0) / (5000.0 - 2000.0), 0.0, 1.0), 1.0))'
'mix(mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 1.0, 0.0, 1.0), clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0)), vec4(0.0, 0.0, 1.0, 1.0), clamp((a_attr - 2000.0) / (5000.0 - 2000.0), 0.0, 1.0))'
);
});

Expand All @@ -948,7 +957,7 @@ describe('ol/style/expressions', function () {
10,
])
).to.eql(
'mix(mix(-10.0, 0.0, pow(clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0), 1.0)), 10.0, pow(clamp((a_attr - 2000.0) / (5000.0 - 2000.0), 0.0, 1.0), 1.0))'
'mix(mix(-10.0, 0.0, clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0)), 10.0, clamp((a_attr - 2000.0) / (5000.0 - 2000.0), 0.0, 1.0))'
);
});

Expand All @@ -966,7 +975,7 @@ describe('ol/style/expressions', function () {
10,
])
).to.eql(
'mix(mix(-10.0, 0.0, pow(clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0), 0.5)), 10.0, pow(clamp((a_attr - 2000.0) / (5000.0 - 2000.0), 0.0, 1.0), 0.5))'
'mix(mix(-10.0, 0.0, clamp((pow(0.5, (a_attr - 1000.0)) - 1.0) / (pow(0.5, (2000.0 - 1000.0)) - 1.0), 0.0, 1.0)), 10.0, clamp((pow(0.5, (a_attr - 2000.0)) - 1.0) / (pow(0.5, (5000.0 - 2000.0)) - 1.0), 0.0, 1.0))'
);
});

Expand Down Expand Up @@ -1086,6 +1095,63 @@ describe('ol/style/expressions', function () {
});
});

describe('geometry-type operator', function () {
let context;

beforeEach(function () {
context = {
variables: [],
attributes: [],
stringLiteralsMap: {},
functions: {},
style: {},
};
});

it('outputs string', function () {
expect(getValueType(['geometry-type'])).to.eql(ValueTypes.STRING);
});

it('throws if invalid argument count', function () {
expect(() => {
expressionToGlsl(context, ['geometry-type', 'abcd']);
}).to.throwException(/0 arguments were expected/);
});

it('correctly parses the expression and add a new attribute', function () {
const glsl = expressionToGlsl(context, ['geometry-type']);
expect(glsl).to.eql('a_geometryType');
expect(context.attributes[0].name).to.be('geometryType');
expect(context.attributes[0].type).to.be(ValueTypes.STRING);
expect(context.attributes[0].callback).to.be.a(Function);
});

describe('geometry type computation', () => {
let features, callback;
beforeEach(() => {
expressionToGlsl(context, ['geometry-type']);
callback = context.attributes[0]['callback'];
features = [
new Feature(new Point([0, 1])),
new Feature(new MultiPolygon([])),
new Feature(new MultiLineString([])),
new Feature(new GeometryCollection([new Circle([0, 1])])),
new Feature(new GeometryCollection([new MultiPoint([])])),
];
});

it('computes a standard geometry type from the given features', function () {
expect(features.map(callback)).to.eql([
'Point',
'Polygon',
'LineString',
'Polygon',
'Point',
]);
});
});
});

describe('complex expressions', function () {
let context;

Expand Down Expand Up @@ -1131,7 +1197,7 @@ describe('ol/style/expressions', function () {
['match', ['get', 'year'], 2000, 'green', '#ffe52c'],
];
expect(expressionToGlsl(context, expression, ValueTypes.COLOR)).to.eql(
'mix(vec4(0.5, 0.5, 0.0, 0.5), (a_year == 2000.0 ? vec4(0.0, 0.5019607843137255, 0.0, 1.0) : vec4(1.0, 0.8980392156862745, 0.17254901960784313, 1.0)), pow(clamp((pow((mod((u_time + mix(0.0, 8.0, pow(clamp((a_year - 1850.0) / (2015.0 - 1850.0), 0.0, 1.0), 1.0))), 8.0) / 8.0), 0.5) - 0.0) / (1.0 - 0.0), 0.0, 1.0), 1.0))'
'mix(vec4(0.5, 0.5, 0.0, 0.5), (a_year == 2000.0 ? vec4(0.0, 0.5019607843137255, 0.0, 1.0) : vec4(1.0, 0.8980392156862745, 0.17254901960784313, 1.0)), clamp((pow((mod((u_time + mix(0.0, 8.0, clamp((a_year - 1850.0) / (2015.0 - 1850.0), 0.0, 1.0))), 8.0) / 8.0), 0.5) - 0.0) / (1.0 - 0.0), 0.0, 1.0))'
);
});

Expand Down
8 changes: 4 additions & 4 deletions test/browser/spec/ol/webgl/styleparser.test.js
Expand Up @@ -48,7 +48,7 @@ describe('ol.webgl.styleparser', function () {
'vec4(vec4(0.2, 0.4, 0.6, 1.0).rgb, vec4(0.2, 0.4, 0.6, 1.0).a * 0.5 * 1.0)'
);
expect(result.builder.symbolSizeExpression_).to.eql(
`vec2(mix(4.0, 8.0, pow(clamp((a_population - ${lowerUniformName}) / (${higherUniformName} - ${lowerUniformName}), 0.0, 1.0), 1.0)))`
`vec2(mix(4.0, 8.0, clamp((a_population - ${lowerUniformName}) / (${higherUniformName} - ${lowerUniformName}), 0.0, 1.0)))`
);
expect(result.builder.symbolOffsetExpression_).to.eql('vec2(0.0, 0.0)');
expect(result.builder.texCoordExpression_).to.eql(
Expand Down Expand Up @@ -273,7 +273,7 @@ describe('ol.webgl.styleparser', function () {
expect(result.builder.attributes_).to.eql([]);
expect(result.builder.varyings_).to.eql([]);
expect(result.builder.symbolColorExpression_).to.eql(
`vec4(mix(vec4(1.0, 1.0, 0.0, 1.0), vec4(1.0, 0.0, 0.0, 1.0), pow(clamp((${uniformName} - 0.0) / (1.0 - 0.0), 0.0, 1.0), 1.0)).rgb, mix(vec4(1.0, 1.0, 0.0, 1.0), vec4(1.0, 0.0, 0.0, 1.0), pow(clamp((${uniformName} - 0.0) / (1.0 - 0.0), 0.0, 1.0), 1.0)).a * 1.0 * 1.0)`
`vec4(mix(vec4(1.0, 1.0, 0.0, 1.0), vec4(1.0, 0.0, 0.0, 1.0), clamp((${uniformName} - 0.0) / (1.0 - 0.0), 0.0, 1.0)).rgb, mix(vec4(1.0, 1.0, 0.0, 1.0), vec4(1.0, 0.0, 0.0, 1.0), clamp((${uniformName} - 0.0) / (1.0 - 0.0), 0.0, 1.0)).a * 1.0 * 1.0)`
);
expect(result.builder.symbolSizeExpression_).to.eql('vec2(6.0)');
expect(result.builder.symbolOffsetExpression_).to.eql('vec2(0.0, 0.0)');
Expand Down Expand Up @@ -328,7 +328,7 @@ describe('ol.webgl.styleparser', function () {
},
]);
expect(result.builder.strokeColorExpression_).to.eql(
'mix(vec4(0.0, 0.0, 1.0, 1.0), vec4(1.0, 0.0, 0.0, 1.0), pow(clamp((v_intensity - 0.0) / (1.0 - 0.0), 0.0, 1.0), 1.0))'
'mix(vec4(0.0, 0.0, 1.0, 1.0), vec4(1.0, 0.0, 0.0, 1.0), clamp((v_intensity - 0.0) / (1.0 - 0.0), 0.0, 1.0))'
);
expect(result.builder.strokeWidthExpression_).to.eql(
'(u_var_width * 3.0)'
Expand Down Expand Up @@ -366,7 +366,7 @@ describe('ol.webgl.styleparser', function () {
},
]);
expect(result.builder.fillColorExpression_).to.eql(
'mix(vec4(0.0, 0.0, 1.0, 1.0), vec4(1.0, 0.0, 0.0, 1.0), pow(clamp(((v_intensity * u_var_scale) - 0.0) / (10.0 - 0.0), 0.0, 1.0), 1.0))'
'mix(vec4(0.0, 0.0, 1.0, 1.0), vec4(1.0, 0.0, 0.0, 1.0), clamp(((v_intensity * u_var_scale) - 0.0) / (10.0 - 0.0), 0.0, 1.0))'
);
expect(Object.keys(result.attributes).length).to.eql(1);
expect(result.attributes).to.have.property('intensity');
Expand Down

0 comments on commit b80fc19

Please sign in to comment.