From 112f0813f7e9d986200ca8936460d6a26b79db88 Mon Sep 17 00:00:00 2001 From: jansule Date: Fri, 21 Jun 2024 10:32:53 +0200 Subject: [PATCH] feat: add interpolate function --- data/mapbox/color_rgba.ts | 8 +- data/mapbox/expression_interpolate.ts | 37 ++++++ .../mapbox_metadata/expression_interpolate.ts | 49 ++++++++ data/styles/color_rgba.ts | 30 ++--- data/styles/gs_expression_case.ts | 5 +- data/styles/gs_expression_decisions.ts | 36 +++--- data/styles/gs_expression_interpolate.ts | 50 +++++++++ src/Expressions.spec.ts | 14 +++ src/Expressions.ts | 106 +++++++++++++----- 9 files changed, 265 insertions(+), 70 deletions(-) create mode 100644 data/mapbox/expression_interpolate.ts create mode 100644 data/mapbox_metadata/expression_interpolate.ts create mode 100644 data/styles/gs_expression_interpolate.ts diff --git a/data/mapbox/color_rgba.ts b/data/mapbox/color_rgba.ts index ed43afb..573e714 100644 --- a/data/mapbox/color_rgba.ts +++ b/data/mapbox/color_rgba.ts @@ -1,8 +1,8 @@ import { MbStyle } from '../../src/MapboxStyleParser'; -const circleSimpleCircle: MbStyle = { +const colorRgba: MbStyle = { version: 8, - name: 'Simple Circle', + name: 'Color RGBA', sources: { testsource: { type: 'vector' @@ -10,7 +10,7 @@ const circleSimpleCircle: MbStyle = { }, layers: [ { - id: 'Simple Circle', + id: 'Color RGBA', source: 'testsource', 'source-layer': 'foo', type: 'circle', @@ -27,4 +27,4 @@ const circleSimpleCircle: MbStyle = { ] }; -export default circleSimpleCircle; +export default colorRgba; diff --git a/data/mapbox/expression_interpolate.ts b/data/mapbox/expression_interpolate.ts new file mode 100644 index 0000000..44d9adf --- /dev/null +++ b/data/mapbox/expression_interpolate.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { MbStyle } from '../../src/MapboxStyleParser'; + +const expressionInterpolate: MbStyle = { + version: 8, + name: 'Expression Interpolate', + sources: { + testsource: { + type: 'vector' + } + }, + layers: [ + { + id: 'earthquake_circle', + type: 'circle', + source: 'testsource', + 'source-layer': 'foo', + paint: { + 'circle-color': '#000000', + 'circle-opacity': 0.6, + 'circle-radius': [ + 'interpolate', + ['linear'], + ['get', 'population'], + 12, + 2, + 15, + 4, + 19, + 35 + ] + } + } + ] +}; + +export default expressionInterpolate; diff --git a/data/mapbox_metadata/expression_interpolate.ts b/data/mapbox_metadata/expression_interpolate.ts new file mode 100644 index 0000000..323f437 --- /dev/null +++ b/data/mapbox_metadata/expression_interpolate.ts @@ -0,0 +1,49 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { MbStyle } from '../../src/MapboxStyleParser'; + +const expression_case: MbStyle = { + version: 8, + name: 'Expression Interpolate', + sources: { + testsource: { + type: 'vector' + } + }, + layers: [ + { + id: 'r0_sy0_st0', + source: 'testsource', + 'source-layer': 'foo', + type: 'circle', + paint: { + 'circle-color': '#000000', + 'circle-opacity': 0.6, + 'circle-radius': [ + 'interpolate', + ['linear'], + ['get', 'population'], + 12, + 2, + 15, + 4, + 19, + 35 + ] + } + } + ], + metadata: { + 'geostyler:ref': { + rules: [{ + name: 'earthquake_circle', + symbolizers: [ + [ + 'r0_sy0_st0' + ] + ] + }] + } + } +}; + +export default expression_case; diff --git a/data/styles/color_rgba.ts b/data/styles/color_rgba.ts index cc7ab1d..86c7864 100644 --- a/data/styles/color_rgba.ts +++ b/data/styles/color_rgba.ts @@ -1,26 +1,26 @@ import { Style } from 'geostyler-style'; -const circleSimpleCircle: Style = { - name: 'Simple Circle', +const colorRgba: Style = { + name: 'Color RGBA', rules: [{ - name: 'Simple Circle', + name: 'Color RGBA', symbolizers: [{ kind: 'Mark', wellKnownName: 'circle', color: '#000000', strokeColor: { name: 'case', - args: [{ - case: { - name: 'lessThan', - args: [{ - name: 'property', - args: ['mag'] - }, 2] - }, - value: '#ff0000' - }, - '#00ff00' + args: [ + '#00ff00', { + case: { + name: 'lessThan', + args: [{ + name: 'property', + args: ['mag'] + }, 2] + }, + value: '#ff0000' + } ] } }] @@ -42,4 +42,4 @@ const circleSimpleCircle: Style = { } }; -export default circleSimpleCircle; +export default colorRgba; diff --git a/data/styles/gs_expression_case.ts b/data/styles/gs_expression_case.ts index 6ee46c4..12cfa88 100644 --- a/data/styles/gs_expression_case.ts +++ b/data/styles/gs_expression_case.ts @@ -10,7 +10,7 @@ const gs_expression_case: Style = { wellKnownName: 'circle', color: { name: 'case', - args: [{ + args: ['#e31a1c', { case: { name: 'lessThan', args: [{ @@ -80,8 +80,7 @@ const gs_expression_case: Style = { }] }, value:'#fc4e2a' - }, - '#e31a1c'] + }] }, radius: 12, fillOpacity: 0.6 diff --git a/data/styles/gs_expression_decisions.ts b/data/styles/gs_expression_decisions.ts index 54d0659..4733d1a 100644 --- a/data/styles/gs_expression_decisions.ts +++ b/data/styles/gs_expression_decisions.ts @@ -11,6 +11,7 @@ const gs_expression_decisions: Style = { color: { name: 'case', args: [ + '#000000', { case: { name: 'not', @@ -23,8 +24,7 @@ const gs_expression_decisions: Style = { }] }, value: '#FFFFFF' - }, - '#000000' + } ] } }] @@ -36,6 +36,7 @@ const gs_expression_decisions: Style = { color: { name: 'case', args: [ + '#000000', { case: { name: 'notEqualTo', @@ -45,8 +46,7 @@ const gs_expression_decisions: Style = { }, 1] }, value: '#FFFFFF' - }, - '#000000' + } ] } }] @@ -58,6 +58,7 @@ const gs_expression_decisions: Style = { color: { name: 'case', args: [ + '#000000', { case: { name: 'lessThan', @@ -67,8 +68,7 @@ const gs_expression_decisions: Style = { }, 1] }, value: '#FFFFFF' - }, - '#000000' + } ] } }] @@ -80,6 +80,7 @@ const gs_expression_decisions: Style = { color: { name: 'case', args: [ + '#000000', { case: { name: 'lessThanOrEqualTo', @@ -89,8 +90,7 @@ const gs_expression_decisions: Style = { }, 1] }, value: '#FFFFFF' - }, - '#000000' + } ] } }] @@ -102,6 +102,7 @@ const gs_expression_decisions: Style = { color: { name: 'case', args: [ + '#000000', { case: { name: 'equalTo', @@ -111,8 +112,7 @@ const gs_expression_decisions: Style = { }, 1] }, value: '#FFFFFF' - }, - '#000000' + } ] } }] @@ -124,6 +124,7 @@ const gs_expression_decisions: Style = { color: { name: 'case', args: [ + '#000000', { case: { name: 'greaterThan', @@ -133,8 +134,7 @@ const gs_expression_decisions: Style = { }, 1] }, value: '#FFFFFF' - }, - '#000000' + } ] } }] @@ -146,6 +146,7 @@ const gs_expression_decisions: Style = { color: { name: 'case', args: [ + '#000000', { case: { name: 'greaterThanOrEqualTo', @@ -155,8 +156,7 @@ const gs_expression_decisions: Style = { }, 1] }, value: '#FFFFFF' - }, - '#000000' + } ] } }] @@ -168,6 +168,7 @@ const gs_expression_decisions: Style = { color: { name: 'case', args: [ + '#000000', { case: { name: 'all', @@ -189,8 +190,7 @@ const gs_expression_decisions: Style = { ] }, value: '#FFFFFF' - }, - '#000000' + } ] } }] @@ -202,6 +202,7 @@ const gs_expression_decisions: Style = { color: { name: 'case', args: [ + '#000000', { case: { name: 'any', @@ -223,8 +224,7 @@ const gs_expression_decisions: Style = { ] }, value: '#FFFFFF' - }, - '#000000' + } ] } }] diff --git a/data/styles/gs_expression_interpolate.ts b/data/styles/gs_expression_interpolate.ts new file mode 100644 index 0000000..a087506 --- /dev/null +++ b/data/styles/gs_expression_interpolate.ts @@ -0,0 +1,50 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { Style } from 'geostyler-style'; + +const gsExpressionInterpolate: Style = { + name: 'Expression Interpolate', + rules: [{ + name: 'earthquake_circle', + symbolizers: [{ + kind: 'Mark', + wellKnownName: 'circle', + color: '#000000', + fillOpacity: 0.6, + radius: { + name: 'interpolate', + args: [{ + name: 'linear' + }, { + name: 'property', + args: ['population'] + }, { + stop: 12, + value: 2 + }, { + stop: 15, + value: 4 + }, { + stop: 19, + value: 35 + }] + } + }] + }], + metadata: { + 'mapbox:ref': { + sources: { + testsource: { + type: 'vector' + } + }, + sourceMapping: { + testsource: [0] + }, + sourceLayerMapping: { + foo: [0] + } + } + } +}; + +export default gsExpressionInterpolate; diff --git a/src/Expressions.spec.ts b/src/Expressions.spec.ts index 78d99e5..b00053a 100644 --- a/src/Expressions.spec.ts +++ b/src/Expressions.spec.ts @@ -15,6 +15,9 @@ import gs_expression_string from '../data/styles/gs_expression_string'; import expression_string_metadata from '../data/mapbox_metadata/expression_string'; import expression_lookup_metadata from '../data/mapbox_metadata/expression_lookup'; import gs_expression_lookup from '../data/styles/gs_expression_lookup'; +import gs_expression_interpolate from '../data/styles/gs_expression_interpolate'; +import expression_interpolate from '../data/mapbox/expression_interpolate'; +import expression_interpolate_metadata from '../data/mapbox_metadata/expression_interpolate'; describe('MapboxStyleParser can parse Expressions', () => { let styleParser: MapboxStyleParser; @@ -60,6 +63,12 @@ describe('MapboxStyleParser can parse Expressions', () => { expect(geoStylerStyle).toEqual(gs_expression_math); return; }); + + it('can read the "interpolate" expressions', async () => { + const { output: geostylerStyle } = await styleParser.readStyle(expression_interpolate); + expect(geostylerStyle).toBeDefined(); + expect(geostylerStyle).toEqual(gs_expression_interpolate); + }); }); describe('#writeStyle', () => { @@ -99,5 +108,10 @@ describe('MapboxStyleParser can parse Expressions', () => { expect(mbStyle).toEqual(expression_string_metadata); return; }); + it('can write the "interpolate" expression', async () => { + const { output: mbStyle } = await styleParser.writeStyle(gs_expression_interpolate); + expect(mbStyle).toBeDefined(); + expect(mbStyle).toEqual(expression_interpolate_metadata); + }); }); }); diff --git a/src/Expressions.ts b/src/Expressions.ts index e27df30..1bb0f86 100644 --- a/src/Expressions.ts +++ b/src/Expressions.ts @@ -1,6 +1,7 @@ import { Expression, Fcase, + Finterpolate, Expression as GeoStylerExpression, GeoStylerFunction, PropertyType, @@ -118,6 +119,7 @@ const functionNameMap: Record strSubstringStart: null, strToLowerCase: 'downcase', strToUpperCase: 'upcase', + strToString: null, strTrim: null, // ---- number ---- add: '+', @@ -131,6 +133,7 @@ const functionNameMap: Record div: '/', exp: 'e', floor: 'floor', + interpolate: 'interpolate', log: 'ln', // – : 'ln2' // – : 'log10' @@ -150,6 +153,7 @@ const functionNameMap: Record sub: '-', tan: 'tan', toDegrees: null, + toNumber: null, toRadians: null, // ---- boolean ---- all: 'all', @@ -169,11 +173,7 @@ const functionNameMap: Record // ---- unknown ---- case: 'case', property: 'get', - // TODO use/translate these new functions if possible - step: null, - interpolate: null, - toNumber: null, - strToString: null + step: null }; const invertedFunctionNameMap: Partial> = @@ -189,19 +189,41 @@ export function gs2mbExpression(gsExpression?: GeoStyler // special handling switch (gsExpression.name) { - case 'case': + case 'case': { + const mbArgs: any = []; + let fallback: any; + args.forEach((arg: any, index: number) => { + if (index === 0) { + fallback = gs2mbExpression(arg); + return; + } + if (arg.case === null || arg.case === undefined || arg.value === null || arg.value === undefined) { + throw new Error('Could not translate GeoStyler Expression: ' + gsExpression); + } + mbArgs.push(gs2mbExpression(arg.case)); + mbArgs.push(gs2mbExpression(arg.value)); + }); + return ['case', ...mbArgs, fallback]; + } + case 'interpolate': { const mbArgs: any = []; args.forEach((arg: any, index: number) => { - if (index === (args.length - 1)) { + if (index === 0) { + mbArgs.push([arg.name]); + return; + } + if (index === 1) { mbArgs.push(gs2mbExpression(arg)); - } else if (arg.case && arg.value) { - mbArgs.push(gs2mbExpression(arg.case)); - mbArgs.push(gs2mbExpression(arg.value)); - } else { - throw new Error('Could not translate GeoStyler Expression: ' + gs2mbExpression); + return; + } + if (arg.stop === null || arg.stop === undefined || arg.value === null || arg.value === undefined) { + throw new Error('Could not translate GeoStyler Expression: ' + gsExpression); } + mbArgs.push(gs2mbExpression(arg.stop)); + mbArgs.push(gs2mbExpression(arg.value)); }); - return ['case', ...mbArgs]; + return ['interpolate', ...mbArgs]; + } case 'exp': if (args[0] === 1) { return ['e']; @@ -247,32 +269,56 @@ export function mb2gsExpression(mbExpression?: MbInput, args: [1] }; break; - case 'case': + case 'case': { const gsArgs: any[] = []; + const fallback = mb2gsExpression(args.pop(), isColor); args.forEach((a, index) => { - if (index < (args.length - 1)) { - var gsIndex = index < 2 ? 0 : Math.floor(index / 2); - if (!gsArgs[gsIndex]) { - gsArgs[gsIndex] = {}; - } - if (index % 2 === 0) { - gsArgs[gsIndex] = { - case: mb2gsExpression(a) - }; - } else { - gsArgs[gsIndex] = { - ...gsArgs[gsIndex] as any, - value: mb2gsExpression(a, isColor) - }; - } + var gsIndex = Math.floor(index / 2); + if (index % 2 === 0) { + gsArgs[gsIndex] = { + case: mb2gsExpression(a) + }; + } else { + gsArgs[gsIndex] = { + ...gsArgs[gsIndex] as any, + value: mb2gsExpression(a, isColor) + }; } }); - gsArgs.push(mb2gsExpression(mbExpression.at(-1), isColor)); + // adding the fallback as the first arg + gsArgs.unshift(fallback); func = { name: 'case', args: gsArgs as Fcase['args'] }; break; + } + case 'interpolate': { + const interpolationType = (args.shift() as [string])[0]; + const input = mb2gsExpression(args.shift()); + const gsArgs: any[] = []; + + args.forEach((a, index) => { + const gsIndex = Math.floor(index / 2); + if (index % 2 === 0) { + gsArgs[gsIndex] = { + stop: mb2gsExpression(a) + }; + } else { + gsArgs[gsIndex] = { + ...gsArgs[gsIndex] as any, + value: mb2gsExpression(a) + }; + } + }); + // adding the interpolation type and the input as the first args + gsArgs.unshift({name: interpolationType}, input); + func = { + name: 'interpolate', + args: gsArgs as Finterpolate['args'] + }; + break; + } case 'pi': func = { name: 'pi'