From 260c80d082ca57c7ecf409b0787b8d00c001954f Mon Sep 17 00:00:00 2001 From: jansule Date: Mon, 12 Feb 2024 15:13:19 +0100 Subject: [PATCH 1/2] feat: read/write labelplacement --- data/slds/1.0/text_lineplacement.sld | 20 ++++++++ data/slds/1.0/text_pointplacement.sld | 20 ++++++++ data/slds/1.1/text_lineplacement.sld | 22 +++++++++ data/slds/1.1/text_pointplacement.sld | 22 +++++++++ data/styles/geoserver/poi.ts | 3 +- data/styles/geoserver/poly_landmarks.ts | 3 +- data/styles/geoserver/pophatch.ts | 3 +- data/styles/geoserver/popshade.ts | 3 +- data/styles/geoserver/tiger_roads.ts | 3 +- data/styles/multi_simplelineLabel.ts | 3 +- data/styles/point_styledLabel_elementOrder.ts | 3 +- ...oint_styledLabel_literalOpenCurlyBraces.ts | 3 +- .../point_styledLabel_literalPlaceholder.ts | 3 +- data/styles/point_styledlabel.ts | 3 +- data/styles/text_lineplacement.ts | 17 +++++++ data/styles/text_pointplacement.ts | 17 +++++++ src/SldStyleParser.geoserver.spec.ts | 30 ++---------- src/SldStyleParser.ts | 49 ++++++++++++++----- src/SldStyleParser.v1.0.spec.ts | 38 +++++++------- src/SldStyleParser.v1.1.spec.ts | 30 ++++++++++++ 20 files changed, 227 insertions(+), 68 deletions(-) create mode 100644 data/slds/1.0/text_lineplacement.sld create mode 100644 data/slds/1.0/text_pointplacement.sld create mode 100644 data/slds/1.1/text_lineplacement.sld create mode 100644 data/slds/1.1/text_pointplacement.sld create mode 100644 data/styles/text_lineplacement.ts create mode 100644 data/styles/text_pointplacement.ts diff --git a/data/slds/1.0/text_lineplacement.sld b/data/slds/1.0/text_lineplacement.sld new file mode 100644 index 00000000..5a873416 --- /dev/null +++ b/data/slds/1.0/text_lineplacement.sld @@ -0,0 +1,20 @@ + + + + Simple Text + + Simple Text + + + + + + + + + + + + + + diff --git a/data/slds/1.0/text_pointplacement.sld b/data/slds/1.0/text_pointplacement.sld new file mode 100644 index 00000000..3949169c --- /dev/null +++ b/data/slds/1.0/text_pointplacement.sld @@ -0,0 +1,20 @@ + + + + Simple Text + + Simple Text + + + + + + + + + + + + + + diff --git a/data/slds/1.1/text_lineplacement.sld b/data/slds/1.1/text_lineplacement.sld new file mode 100644 index 00000000..cf963920 --- /dev/null +++ b/data/slds/1.1/text_lineplacement.sld @@ -0,0 +1,22 @@ + + + + Simple Text + + Simple Text + + + + + + myText + + + + + + + + + + diff --git a/data/slds/1.1/text_pointplacement.sld b/data/slds/1.1/text_pointplacement.sld new file mode 100644 index 00000000..3876b906 --- /dev/null +++ b/data/slds/1.1/text_pointplacement.sld @@ -0,0 +1,22 @@ + + + + Simple Text + + Simple Text + + + + + + myText + + + + + + + + + + diff --git a/data/styles/geoserver/poi.ts b/data/styles/geoserver/poi.ts index c1408b60..e2c53fa4 100644 --- a/data/styles/geoserver/poi.ts +++ b/data/styles/geoserver/poi.ts @@ -43,7 +43,8 @@ const style: Style = { 'Arial' ], fontWeight: 'bold', - size: 14 + size: 14, + placement: 'point' } ] } diff --git a/data/styles/geoserver/poly_landmarks.ts b/data/styles/geoserver/poly_landmarks.ts index 5f92a6cc..d71464a0 100644 --- a/data/styles/geoserver/poly_landmarks.ts +++ b/data/styles/geoserver/poly_landmarks.ts @@ -155,7 +155,8 @@ const style: Style = { ], fontStyle: 'normal', fontWeight: 'bold', - size: 14 + size: 14, + placement: 'point' } ] } diff --git a/data/styles/geoserver/pophatch.ts b/data/styles/geoserver/pophatch.ts index 10a35a04..7ee1530a 100644 --- a/data/styles/geoserver/pophatch.ts +++ b/data/styles/geoserver/pophatch.ts @@ -78,7 +78,8 @@ const style: Style = { 'Times New Roman' ], fontStyle: 'normal', - size: 14 + size: 14, + placement: 'point' } ] } diff --git a/data/styles/geoserver/popshade.ts b/data/styles/geoserver/popshade.ts index cfbe29e1..63ce8f7e 100644 --- a/data/styles/geoserver/popshade.ts +++ b/data/styles/geoserver/popshade.ts @@ -65,7 +65,8 @@ const style: Style = { 'Times New Roman' ], fontStyle: 'normal', - size: 14 + size: 14, + placement: 'point' } ] } diff --git a/data/styles/geoserver/tiger_roads.ts b/data/styles/geoserver/tiger_roads.ts index 724464fb..74af2ee5 100644 --- a/data/styles/geoserver/tiger_roads.ts +++ b/data/styles/geoserver/tiger_roads.ts @@ -61,7 +61,8 @@ const style: Style = { ], fontStyle: 'normal', fontWeight: 'bold', - size: 14 + size: 14, + placement: 'line' } ] } diff --git a/data/styles/multi_simplelineLabel.ts b/data/styles/multi_simplelineLabel.ts index 5aa2cb6e..0915c943 100644 --- a/data/styles/multi_simplelineLabel.ts +++ b/data/styles/multi_simplelineLabel.ts @@ -18,7 +18,8 @@ const multiSimplelineLabel: Style = { size: 12, offset: [0, 5], fontStyle: 'normal', - fontWeight: 'bold' + fontWeight: 'bold', + placement: 'point' }] }] }; diff --git a/data/styles/point_styledLabel_elementOrder.ts b/data/styles/point_styledLabel_elementOrder.ts index 406bb4b3..cab6c787 100644 --- a/data/styles/point_styledLabel_elementOrder.ts +++ b/data/styles/point_styledLabel_elementOrder.ts @@ -16,7 +16,8 @@ const pointStyledLabel: Style = { opacity: 1, rotate: 45, fontStyle: 'normal', - fontWeight: 'bold' + fontWeight: 'bold', + placement: 'point' }] }] }; diff --git a/data/styles/point_styledLabel_literalOpenCurlyBraces.ts b/data/styles/point_styledLabel_literalOpenCurlyBraces.ts index b0e93697..329447d3 100644 --- a/data/styles/point_styledLabel_literalOpenCurlyBraces.ts +++ b/data/styles/point_styledLabel_literalOpenCurlyBraces.ts @@ -16,7 +16,8 @@ const pointStyledLabel: Style = { opacity: 1, rotate: 45, fontStyle: 'normal', - fontWeight: 'bold' + fontWeight: 'bold', + placement: 'point' }] }] }; diff --git a/data/styles/point_styledLabel_literalPlaceholder.ts b/data/styles/point_styledLabel_literalPlaceholder.ts index 4a784314..4ad0aafa 100644 --- a/data/styles/point_styledLabel_literalPlaceholder.ts +++ b/data/styles/point_styledLabel_literalPlaceholder.ts @@ -17,7 +17,8 @@ const pointStyledLabel: Style = { opacity: 1, rotate: 45, fontStyle: 'normal', - fontWeight: 'bold' + fontWeight: 'bold', + placement: 'point' }] }] }; diff --git a/data/styles/point_styledlabel.ts b/data/styles/point_styledlabel.ts index b208b921..b045251b 100644 --- a/data/styles/point_styledlabel.ts +++ b/data/styles/point_styledlabel.ts @@ -17,7 +17,8 @@ const pointStyledLabel: Style = { haloOpacity: 1, rotate: 45, fontStyle: 'normal', - fontWeight: 'bold' + fontWeight: 'bold', + placement: 'point' }] }] }; diff --git a/data/styles/text_lineplacement.ts b/data/styles/text_lineplacement.ts new file mode 100644 index 00000000..f1223fee --- /dev/null +++ b/data/styles/text_lineplacement.ts @@ -0,0 +1,17 @@ +import { Style } from 'geostyler-style'; + +const pointStyledLabel: Style = { + name: 'Simple Text', + rules: [{ + name: '', + symbolizers: [{ + color: '#000000', + opacity: 1, + kind: 'Text', + label: 'myText', + placement: 'line' + }] + }] +}; + +export default pointStyledLabel; diff --git a/data/styles/text_pointplacement.ts b/data/styles/text_pointplacement.ts new file mode 100644 index 00000000..4de02e32 --- /dev/null +++ b/data/styles/text_pointplacement.ts @@ -0,0 +1,17 @@ +import { Style } from 'geostyler-style'; + +const pointStyledLabel: Style = { + name: 'Simple Text', + rules: [{ + name: '', + symbolizers: [{ + color: '#000000', + opacity: 1, + kind: 'Text', + label: 'myText', + placement: 'point' + }] + }] +}; + +export default pointStyledLabel; diff --git a/src/SldStyleParser.geoserver.spec.ts b/src/SldStyleParser.geoserver.spec.ts index 0be1cec7..f9da264d 100644 --- a/src/SldStyleParser.geoserver.spec.ts +++ b/src/SldStyleParser.geoserver.spec.ts @@ -399,14 +399,10 @@ describe('SldStyleParser implements StyleParser', () => { it('can write the geoserver poi.sld', async () => { const { output: sldString, - errors, - warnings, - unsupportedProperties + errors } = await styleParser.writeStyle(poi); expect(sldString).toBeDefined(); expect(errors).toBeUndefined(); - expect(warnings).toBeUndefined(); - expect(unsupportedProperties).toBeUndefined(); // As string comparison between two XML-Strings is awkward and nonsens // we read it again and compare the json input with the parser output const { output: readStyle} = await styleParser.readStyle(sldString!); @@ -415,14 +411,10 @@ describe('SldStyleParser implements StyleParser', () => { it('can write the geoserver poly_landmarks.sld', async () => { const { output: sldString, - errors, - warnings, - unsupportedProperties + errors } = await styleParser.writeStyle(poly_landmarks); expect(sldString).toBeDefined(); expect(errors).toBeUndefined(); - expect(warnings).toBeUndefined(); - expect(unsupportedProperties).toBeUndefined(); // As string comparison between two XML-Strings is awkward and nonsens // we read it again and compare the json input with the parser output const { output: readStyle} = await styleParser.readStyle(sldString!); @@ -431,14 +423,10 @@ describe('SldStyleParser implements StyleParser', () => { it('can write the geoserver pophatch.sld', async () => { const { output: sldString, - errors, - warnings, - unsupportedProperties + errors } = await styleParser.writeStyle(pophatch); expect(sldString).toBeDefined(); expect(errors).toBeUndefined(); - expect(warnings).toBeUndefined(); - expect(unsupportedProperties).toBeUndefined(); // As string comparison between two XML-Strings is awkward and nonsens // we read it again and compare the json input with the parser output const { output: readStyle} = await styleParser.readStyle(sldString!); @@ -447,14 +435,10 @@ describe('SldStyleParser implements StyleParser', () => { it('can write the geoserver popshade.sld', async () => { const { output: sldString, - errors, - warnings, - unsupportedProperties + errors } = await styleParser.writeStyle(popshade); expect(sldString).toBeDefined(); expect(errors).toBeUndefined(); - expect(warnings).toBeUndefined(); - expect(unsupportedProperties).toBeUndefined(); // As string comparison between two XML-Strings is awkward and nonsens // we read it again and compare the json input with the parser output const { output: readStyle} = await styleParser.readStyle(sldString!); @@ -543,14 +527,10 @@ describe('SldStyleParser implements StyleParser', () => { it('can write the geoserver tiger_roads.sld', async () => { const { output: sldString, - errors, - warnings, - unsupportedProperties + errors } = await styleParser.writeStyle(tiger_roads); expect(sldString).toBeDefined(); expect(errors).toBeUndefined(); - expect(warnings).toBeUndefined(); - expect(unsupportedProperties).toBeUndefined(); // As string comparison between two XML-Strings is awkward and nonsens // we read it again and compare the json input with the parser output const { output: readStyle} = await styleParser.readStyle(sldString!); diff --git a/src/SldStyleParser.ts b/src/SldStyleParser.ts index ec4aff4f..9ac0c9e6 100644 --- a/src/SldStyleParser.ts +++ b/src/SldStyleParser.ts @@ -178,6 +178,12 @@ export class SldStyleParser implements StyleParser { resampling: 'none', saturation: 'none', visibility: 'none' + }, + TextSymbolizer: { + placement: { + support: 'partial', + info: 'Only "line" and "point" are currently supported' + } } } }; @@ -695,18 +701,28 @@ export class SldStyleParser implements StyleParser { if (!isNil(haloColor)) { textSymbolizer.haloColor = haloColor; } - const displacement = get(sldSymbolizer, 'LabelPlacement.PointPlacement.Displacement'); - if (!isNil(displacement)) { - const x = get(displacement, 'DisplacementX.#text'); - const y = get(displacement, 'DisplacementY.#text'); - textSymbolizer.offset = [ - Number.isFinite(x) ? numberExpression(x) : 0, - Number.isFinite(y) ? numberExpression(y) : 0, - ]; - } - const rotation = get(sldSymbolizer, 'LabelPlacement.PointPlacement.Rotation.#text'); - if (!isNil(rotation)) { - textSymbolizer.rotate = numberExpression(rotation); + const placement = get(sldSymbolizer, 'LabelPlacement'); + if (!isNil(placement)) { + const pointPlacement = get(placement, 'PointPlacement'); + const linePlacement = get(placement, 'LinePlacement'); + if (!isNil(pointPlacement)) { + textSymbolizer.placement = 'point'; + const displacement = get(placement, 'PointPlacement.Displacement'); + if (!isNil(displacement)) { + const x = get(displacement, 'DisplacementX.#text'); + const y = get(displacement, 'DisplacementY.#text'); + textSymbolizer.offset = [ + Number.isFinite(x) ? numberExpression(x) : 0, + Number.isFinite(y) ? numberExpression(y) : 0, + ]; + } + const rotation = get(placement, 'PointPlacement.Rotation.#text'); + if (!isNil(rotation)) { + textSymbolizer.rotate = numberExpression(rotation); + } + } else if (!isNil(linePlacement)) { + textSymbolizer.placement = 'line'; + } } if (!isNil(fontFamily)) { textSymbolizer.font = [fontFamily]; @@ -1807,6 +1823,7 @@ export class SldStyleParser implements StyleParser { const DisplacementY = this.getTagName('DisplacementY'); const LabelPlacement = this.getTagName('LabelPlacement'); const PointPlacement = this.getTagName('PointPlacement'); + const LinePlacement = this.getTagName('LinePlacement'); const Rotation = this.getTagName('Rotation'); const Radius = this.getTagName('Radius'); const Label = this.getTagName('Label'); @@ -1854,7 +1871,13 @@ export class SldStyleParser implements StyleParser { }); } - if (textSymbolizer.offset || textSymbolizer.rotate !== undefined) { + if (textSymbolizer.placement === 'line') { + sldTextSymbolizer.push({ + [LabelPlacement]: [{ + [LinePlacement]: [] + }] + }); + } else if (textSymbolizer.offset || textSymbolizer.rotate !== undefined || textSymbolizer.placement === 'point') { const pointPlacement: any = []; if (textSymbolizer.offset) { pointPlacement.push({ diff --git a/src/SldStyleParser.v1.0.spec.ts b/src/SldStyleParser.v1.0.spec.ts index 4218d7be..19d13c13 100644 --- a/src/SldStyleParser.v1.0.spec.ts +++ b/src/SldStyleParser.v1.0.spec.ts @@ -40,6 +40,8 @@ import point_styledLabel_elementOrder from '../data/styles/point_styledLabel_ele import raster_simpleraster from '../data/styles/raster_simpleRaster'; import raster_complexraster from '../data/styles/raster_complexRaster'; import raster_without_opacity from '../data/styles/raster_without_opacity'; +import text_pointplacement from '../data/styles/text_pointplacement'; +import text_lineplacement from '../data/styles/text_lineplacement'; import unsupported_properties from '../data/styles/unsupported_properties'; import function_markSymbolizer from '../data/styles/function_markSymbolizer'; import function_filter from '../data/styles/function_filter'; @@ -192,6 +194,18 @@ describe('SldStyleParser implements StyleParser', () => { expect(geoStylerStyle).toBeDefined(); expect(geoStylerStyle).toEqual(point_simpleLabel); }); + it('can read a SLD TextSymbolizer with point placement', async () => { + const sld = fs.readFileSync('./data/slds/1.0/text_pointplacement.sld', 'utf8'); + const { output: geoStylerStyle } = await styleParser.readStyle(sld); + expect(geoStylerStyle).toBeDefined(); + expect(geoStylerStyle).toEqual(text_pointplacement); + }); + it('can read a SLD TextSymbolizer with line placement', async () => { + const sld = fs.readFileSync('./data/slds/1.0/text_lineplacement.sld', 'utf8'); + const { output: geoStylerStyle } = await styleParser.readStyle(sld); + expect(geoStylerStyle).toBeDefined(); + expect(geoStylerStyle).toEqual(text_lineplacement); + }); it('can read a SLD TextSymbolizer with a static label and styling', async () => { const sld = fs.readFileSync('./data/slds/1.0/point_simpleLabel2.sld', 'utf8'); const { output: geoStylerStyle } = await styleParser.readStyle(sld); @@ -661,14 +675,10 @@ describe('SldStyleParser implements StyleParser', () => { it('can write a SLD TextSymbolizer', async () => { const { output: sldString, - errors, - warnings, - unsupportedProperties + errors } = await styleParser.writeStyle(point_styledlabel); expect(sldString).toBeDefined(); expect(errors).toBeUndefined(); - expect(warnings).toBeUndefined(); - expect(unsupportedProperties).toBeUndefined(); // As string comparison between two XML-Strings is awkward and nonsens // we read it again and compare the json input with the parser output const { output: readStyle } = await styleParser.readStyle(sldString!); @@ -827,14 +837,10 @@ describe('SldStyleParser implements StyleParser', () => { it('can write a SLD style with multiple symbolizers in one Rule', async () => { const { output: sldString, - errors, - warnings, - unsupportedProperties + errors } = await styleParser.writeStyle(multi_simplelineLabel); expect(sldString).toBeDefined(); expect(errors).toBeUndefined(); - expect(warnings).toBeUndefined(); - expect(unsupportedProperties).toBeUndefined(); // As string comparison between two XML-Strings is awkward and nonsens // we read it again and compare the json input with the parser output const { output: readStyle } = await styleParser.readStyle(sldString!); @@ -843,14 +849,10 @@ describe('SldStyleParser implements StyleParser', () => { it('can write a SLD style with a styled label containing open curly braces as static text', async () => { const { output: sldString, - errors, - warnings, - unsupportedProperties + errors } = await styleParser.writeStyle(point_styledLabel_literalOpenCurlyBraces); expect(sldString).toBeDefined(); expect(errors).toBeUndefined(); - expect(warnings).toBeUndefined(); - expect(unsupportedProperties).toBeUndefined(); // As string comparison between two XML-Strings is awkward and nonsens // we read it again and compare the json input with the parser output const { output: readStyle} = await styleParser.readStyle(sldString!); @@ -859,14 +861,10 @@ describe('SldStyleParser implements StyleParser', () => { it('can write a SLD style with a styled label containing placeholders and static text', async () => { const { output: sldString, - errors, - warnings, - unsupportedProperties + errors } = await styleParser.writeStyle(point_styledLabel_literalPlaceholder); expect(sldString).toBeDefined(); expect(errors).toBeUndefined(); - expect(warnings).toBeUndefined(); - expect(unsupportedProperties).toBeUndefined(); // As string comparison between two XML-Strings is awkward and nonsens // we read it again and compare the json input with the parser output const { output: readStyle } = await styleParser.readStyle(sldString!); diff --git a/src/SldStyleParser.v1.1.spec.ts b/src/SldStyleParser.v1.1.spec.ts index 27b0bc01..f9333a70 100644 --- a/src/SldStyleParser.v1.1.spec.ts +++ b/src/SldStyleParser.v1.1.spec.ts @@ -41,6 +41,8 @@ import point_styledLabel_literalPlaceholder from '../data/styles/point_styledLab import point_styledLabel_elementOrder from '../data/styles/point_styledLabel_elementOrder'; import raster_simpleraster from '../data/styles/raster_simpleRaster'; import raster_complexraster from '../data/styles/raster_complexRaster'; +import text_pointplacement from '../data/styles/text_pointplacement'; +import text_lineplacement from '../data/styles/text_lineplacement'; import unsupported_properties from '../data/styles/unsupported_properties'; import function_markSymbolizer from '../data/styles/function_markSymbolizer'; import function_filter from '../data/styles/function_filter'; @@ -213,6 +215,18 @@ describe('SldStyleParser with Symbology Encoding implements StyleParser', () => expect(geoStylerStyle).toBeDefined(); expect(geoStylerStyle).toEqual(point_simpleLabel2); }); + it('can read a SLD 1.1 TextSymbolizer with a static label and point placement', async () => { + const sld = fs.readFileSync('./data/slds/1.1/text_pointplacement.sld', 'utf8'); + const { output: geoStylerStyle} = await styleParser.readStyle(sld); + expect(geoStylerStyle).toBeDefined(); + expect(geoStylerStyle).toEqual(text_pointplacement); + }); + it('can read a SLD 1.1 TextSymbolizer with a static label and line placement', async () => { + const sld = fs.readFileSync('./data/slds/1.1/text_lineplacement.sld', 'utf8'); + const { output: geoStylerStyle} = await styleParser.readStyle(sld); + expect(geoStylerStyle).toBeDefined(); + expect(geoStylerStyle).toEqual(text_lineplacement); + }); it('can read a simple SLD 1.1 RasterSymbolizer', async () => { const sld = fs.readFileSync('./data/slds/1.1/raster_simpleRaster.sld', 'utf8'); const { output: geoStylerStyle} = await styleParser.readStyle(sld); @@ -537,6 +551,22 @@ describe('SldStyleParser with Symbology Encoding implements StyleParser', () => const { output: readStyle} = await styleParser.readStyle(sldString!); expect(readStyle).toEqual(point_styledlabel); }); + it('can write a SLD 1.1 TextSymbolizer with point placement', async () => { + const { output: sldString } = await styleParser.writeStyle(text_pointplacement); + expect(sldString).toBeDefined(); + // As string comparison between two XML-Strings is awkward and nonsens + // we read it again and compare the json input with the parser output + const { output: readStyle} = await styleParser.readStyle(sldString!); + expect(readStyle).toEqual(text_pointplacement); + }); + it('can write a SLD 1.1 TextSymbolizer with line placement', async () => { + const { output: sldString } = await styleParser.writeStyle(text_lineplacement); + expect(sldString).toBeDefined(); + // As string comparison between two XML-Strings is awkward and nonsens + // we read it again and compare the json input with the parser output + const { output: readStyle} = await styleParser.readStyle(sldString!); + expect(readStyle).toEqual(text_lineplacement); + }); it('can write a simple SLD RasterSymbolizer', async () => { const { output: sldString } = await styleParser.writeStyle(raster_simpleraster); expect(sldString).toBeDefined(); From 9c455a64287da0dbd7ebc628705d7b575f52236b Mon Sep 17 00:00:00 2001 From: jansule Date: Mon, 12 Feb 2024 17:06:59 +0100 Subject: [PATCH 2/2] refactor: use existing vars for subpaths --- src/SldStyleParser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SldStyleParser.ts b/src/SldStyleParser.ts index 9ac0c9e6..19809e91 100644 --- a/src/SldStyleParser.ts +++ b/src/SldStyleParser.ts @@ -707,7 +707,7 @@ export class SldStyleParser implements StyleParser { const linePlacement = get(placement, 'LinePlacement'); if (!isNil(pointPlacement)) { textSymbolizer.placement = 'point'; - const displacement = get(placement, 'PointPlacement.Displacement'); + const displacement = get(pointPlacement, 'Displacement'); if (!isNil(displacement)) { const x = get(displacement, 'DisplacementX.#text'); const y = get(displacement, 'DisplacementY.#text'); @@ -716,7 +716,7 @@ export class SldStyleParser implements StyleParser { Number.isFinite(y) ? numberExpression(y) : 0, ]; } - const rotation = get(placement, 'PointPlacement.Rotation.#text'); + const rotation = get(pointPlacement, 'Rotation.#text'); if (!isNil(rotation)) { textSymbolizer.rotate = numberExpression(rotation); }