diff --git a/examples/webgl-points-layer.js b/examples/webgl-points-layer.js index 47bba2a1e16..b6acf63052d 100644 --- a/examples/webgl-points-layer.js +++ b/examples/webgl-points-layer.js @@ -13,7 +13,7 @@ const vectorSource = new Vector({ }); const predefinedStyles = { - 'icons': { + icons: { symbol: { symbolType: 'image', src: 'data/icon.png', @@ -23,7 +23,7 @@ const predefinedStyles = { offset: [0, 9], }, }, - 'triangles': { + triangles: { symbol: { symbolType: 'triangle', size: 18, @@ -68,7 +68,7 @@ const predefinedStyles = { opacity: 0.95, }, }, - 'circles': { + circles: { symbol: { symbolType: 'circle', size: [ @@ -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, diff --git a/src/ol/style/expressions.js b/src/ol/style/expressions.js index 1aa1bbcb856..28b0b1302e5 100644 --- a/src/ol/style/expressions.js +++ b/src/ol/style/expressions.js @@ -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 @@ -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 */ /** @@ -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; @@ -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; }, diff --git a/src/ol/webgl/styleparser.js b/src/ol/webgl/styleparser.js index 598883dacb5..f3deae41408 100644 --- a/src/ol/webgl/styleparser.js +++ b/src/ol/webgl/styleparser.js @@ -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) { diff --git a/test/browser/spec/ol/style/expressions.test.js b/test/browser/spec/ol/style/expressions.test.js index a75f2fed166..5d6850c8113 100644 --- a/test/browser/spec/ol/style/expressions.test.js +++ b/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, @@ -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( @@ -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))' ); }); @@ -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))' ); }); @@ -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))' ); }); @@ -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; @@ -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))' ); }); diff --git a/test/browser/spec/ol/webgl/styleparser.test.js b/test/browser/spec/ol/webgl/styleparser.test.js index 08c8531fa3e..542fe0e089d 100644 --- a/test/browser/spec/ol/webgl/styleparser.test.js +++ b/test/browser/spec/ol/webgl/styleparser.test.js @@ -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( @@ -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)'); @@ -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)' @@ -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');