From be950eae89dd262e2018fbdca9fab6ceba75d83d Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 13 Jan 2025 14:29:39 +0100 Subject: [PATCH] feat: add vendor-option graphic-margin (#971) --- data/slds/geoserver/pattern_polygon.sld | 37 ++++++++++++ data/styles/geoserver/default_polygon.ts | 1 - data/styles/geoserver/pattern_polygon.ts | 24 ++++++++ package-lock.json | 8 +-- package.json | 2 +- src/SldStyleParser.geoserver.spec.ts | 22 +++++++- src/SldStyleParser.ts | 71 ++++++++++++++++++++++-- src/Util/SldUtil.ts | 53 +++++++++++------- 8 files changed, 183 insertions(+), 35 deletions(-) create mode 100644 data/slds/geoserver/pattern_polygon.sld create mode 100644 data/styles/geoserver/pattern_polygon.ts diff --git a/data/slds/geoserver/pattern_polygon.sld b/data/slds/geoserver/pattern_polygon.sld new file mode 100644 index 00000000..9ccea0f4 --- /dev/null +++ b/data/slds/geoserver/pattern_polygon.sld @@ -0,0 +1,37 @@ + + + + + Pattern polygon + + + pattern_polygon + Pattern polygon + Polygon with spaced purple circle symbols + + + Polygon with spaced purple circle symbols + Polygon with spaced purple circle symbols + + 4,6,2,3 + + + + + circle + + #880088 + + + 6 + + + + + + + + + diff --git a/data/styles/geoserver/default_polygon.ts b/data/styles/geoserver/default_polygon.ts index 251115fb..463e74d6 100644 --- a/data/styles/geoserver/default_polygon.ts +++ b/data/styles/geoserver/default_polygon.ts @@ -17,5 +17,4 @@ const style: Style = { ] }; - export default style; diff --git a/data/styles/geoserver/pattern_polygon.ts b/data/styles/geoserver/pattern_polygon.ts new file mode 100644 index 00000000..a70657ee --- /dev/null +++ b/data/styles/geoserver/pattern_polygon.ts @@ -0,0 +1,24 @@ +import { Style } from 'geostyler-style'; + +const style: Style = { + name: 'pattern_polygon', + rules: [ + { + name: 'Polygon with spaced purple circle symbols', + symbolizers: [ + { + kind: 'Fill', + graphicFill: { + kind: 'Mark', + wellKnownName: 'circle', + color: '#880088', + radius: 3 + }, + graphicFillPadding: [4, 6, 2, 3], + } + ] + } + ] +}; + +export default style; diff --git a/package-lock.json b/package-lock.json index 56dbd7b1..110b76f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "BSD-2-Clause", "dependencies": { "fast-xml-parser": "^4.4.1", - "geostyler-style": "^9.1.0", + "geostyler-style": "^9.2.0", "lodash": "^4.17.21" }, "devDependencies": { @@ -4586,9 +4586,9 @@ } }, "node_modules/geostyler-style": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-9.1.0.tgz", - "integrity": "sha512-kExQDe2mf4YaVMZPKE7h2uxU5qSyAQoX2U2hLXyAWubJjeNoFe0nxt6rKn7C9Q1DE/9DVZopOdNLCXMtqOE6QA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-9.2.0.tgz", + "integrity": "sha512-LwYkkbgD6VIGEbqcvG7U5hqgWrpWnwTdYtltIGt7bo+toKW9JcSoO6rIpbVPkbpblXJRWYQlbv80D8q3pqo+JQ==", "engines": { "node": ">=20.6.0", "npm": ">=10.0.0" diff --git a/package.json b/package.json index 5ad3a7a1..80d635e4 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "fast-xml-parser": "^4.4.1", - "geostyler-style": "^9.1.0", + "geostyler-style": "^9.2.0", "lodash": "^4.17.21" }, "devDependencies": { diff --git a/src/SldStyleParser.geoserver.spec.ts b/src/SldStyleParser.geoserver.spec.ts index fe157b7e..e611580d 100644 --- a/src/SldStyleParser.geoserver.spec.ts +++ b/src/SldStyleParser.geoserver.spec.ts @@ -27,6 +27,7 @@ import restricted from '../data/styles/geoserver/restricted'; import simple_streams from '../data/styles/geoserver/simple_streams'; import simpleRoads from '../data/styles/geoserver/simpleRoads'; import tiger_roads from '../data/styles/geoserver/tiger_roads'; +import pattern_polygon from '../data/styles/geoserver/pattern_polygon'; it('SldStyleParser is defined', () => { expect(SldStyleParser).toBeDefined(); @@ -36,7 +37,7 @@ describe('SldStyleParser implements StyleParser', () => { let styleParser: SldStyleParser; beforeEach(() => { - styleParser = new SldStyleParser({sldVersion: '1.0.0'}); + styleParser = new SldStyleParser({sldVersion: '1.0.0', withGeoServerVendorOption: true}); }); describe('#readStyle', () => { @@ -178,6 +179,12 @@ describe('SldStyleParser implements StyleParser', () => { expect(geoStylerStyle).toBeDefined(); expect(geoStylerStyle).toEqual(tiger_roads); }); + it('can read the geoserver pattern_polygon.sld', async () => { + const sld = fs.readFileSync('./data/slds/geoserver/pattern_polygon.sld', 'utf8'); + const { output: geoStylerStyle } = await styleParser.readStyle(sld); + expect(geoStylerStyle).toBeDefined(); + expect(geoStylerStyle).toEqual(pattern_polygon); + }); }); describe('#writeStyle', () => { @@ -536,7 +543,18 @@ describe('SldStyleParser implements StyleParser', () => { const { output: readStyle} = await styleParser.readStyle(sldString!); expect(readStyle).toEqual(tiger_roads); }); - + it('can write the geoserver pattern_polygon.sld', async () => { + const { + output: sldString, + errors + } = await styleParser.writeStyle(pattern_polygon); + expect(sldString).toBeDefined(); + expect(errors).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!); + expect(readStyle).toEqual(pattern_polygon); + }); }); }); diff --git a/src/SldStyleParser.ts b/src/SldStyleParser.ts index 98903414..5399775b 100644 --- a/src/SldStyleParser.ts +++ b/src/SldStyleParser.ts @@ -44,6 +44,7 @@ import { getAttribute, getChildren, getParameterValue, + getVendorOptionValue, isSymbolizer, keysByValue, numberExpression @@ -80,6 +81,7 @@ export type ConstructorParams = { boolFilterFields?: string[]; /* optional for reading style (it will be guessed from sld style) and mandatory for writing */ sldVersion?: SldVersion; + withGeoServerVendorOption?: boolean; symbolizerUnits?: string; parserOptions?: ParserOptions; builderOptions?: XmlBuilderOptions; @@ -275,6 +277,7 @@ export class SldStyleParser implements StyleParser { preserveOrder: true, trimValues: true }); + this.builder = new XMLBuilder({ ...opts?.builderOptions, // Fixed attributes @@ -283,10 +286,15 @@ export class SldStyleParser implements StyleParser { suppressEmptyNode: true, preserveOrder: true }); + if (opts?.sldVersion) { this.sldVersion = opts?.sldVersion; } + if (opts?.withGeoServerVendorOption !== undefined) { + this.withGeoServerVendorOption = opts.withGeoServerVendorOption; + } + if (opts?.locale) { this.locale = opts.locale; } @@ -386,6 +394,26 @@ export class SldStyleParser implements StyleParser { this._sldVersion = sldVersion; } + /** + * Indicates whether additional GeoServer vendorOption should be included in + * sld write/parse operations. Set to `false` by default. + */ + private _withGeoServerVendorOption = false; + + /** + * Getter for _withGeoServerVendorOption + */ + get withGeoServerVendorOption(): boolean { + return this._withGeoServerVendorOption; + } + + /** + * Setter for _withGeoServerVendorOption + */ + set withGeoServerVendorOption(withVendorOption: boolean) { + this._withGeoServerVendorOption = withVendorOption; + } + /** * String indicating the SLD version used in reading mode @@ -959,6 +987,12 @@ export class SldStyleParser implements StyleParser { graphicFill ); } + if (this.withGeoServerVendorOption) { + const graphicFillPadding = getVendorOptionValue(sldSymbolizer, 'graphic-margin'); + if (!isNil(graphicFillPadding)) { + fillSymbolizer.graphicFillPadding = graphicFillPadding.split(',').map(numberExpression); + } + } if (!isNil(color)) { fillSymbolizer.color = color; } @@ -1865,6 +1899,30 @@ export class SldStyleParser implements StyleParser { }]; } + /** + * Push a new GeoServerVendorOption in the given array if such options are allowed. + */ + pushGeoServerVendorOption(elementArray: any[], name: string, text: string) { + if (this.withGeoServerVendorOption) { + elementArray.push(this.createGeoServerVendorOption(name, text)); + } + } + + /** + * @returns text + */ + createGeoServerVendorOption(name: string, text: string) { + const VendorOption = this.getTagName('VendorOption'); + return { + [VendorOption]: [{ + '#text': text, + }], + ':@': { + '@_name': name, + } + }; + } + /** * Get the SLD Object (readable with fast-xml-parser) from a geostyler-style IconSymbolizer. * @@ -2454,16 +2512,17 @@ export class SldStyleParser implements StyleParser { const polygonSymbolizer: any = []; if (fillCssParameters.length > 0 || graphicFill) { - if (!Array.isArray(polygonSymbolizer?.[0]?.[Fill])) { - polygonSymbolizer[0] = { [Fill]: [] }; + const fillArray: any[] = []; + const graphicFillPadding = fillSymbolizer.graphicFillPadding; + if (graphicFillPadding) { + this.pushGeoServerVendorOption(polygonSymbolizer, 'graphic-margin', `${graphicFillPadding}`); } + polygonSymbolizer.push({ [Fill]: fillArray }); if (fillCssParameters.length > 0) { - polygonSymbolizer[0][Fill].push(...fillCssParameters); + fillArray.push(...fillCssParameters); } if (graphicFill) { - polygonSymbolizer[0][Fill].push({ - GraphicFill: graphicFill - }); + fillArray.push({ GraphicFill: graphicFill }); } } diff --git a/src/Util/SldUtil.ts b/src/Util/SldUtil.ts index 37e7a20c..a989f24d 100644 --- a/src/Util/SldUtil.ts +++ b/src/Util/SldUtil.ts @@ -7,7 +7,7 @@ import { isGeoStylerFunction, isGeoStylerNumberFunction } from 'geostyler-style/ * Cast to Number if it is not a GeoStylerFunction * * @param exp The GeoStylerExpression - * @returns The value casted to a number or the GeoStylerNumberFunction + * @returns The value cast to a number or the GeoStylerNumberFunction */ export function numberExpression(exp: Expression): GeoStylerNumberFunction | number { return isGeoStylerNumberFunction(exp) ? exp : Number(exp); @@ -56,14 +56,12 @@ export function geoStylerFunctionToSldFunction(geostylerFunction: GeoStylerFunct } }); - const sldFunctionObj = [{ + return [{ Function: sldFunctionArgs, ':@': { '@_name': name } }]; - - return sldFunctionObj; } /** @@ -99,7 +97,7 @@ export function sldFunctionToGeoStylerFunction(sldFunction: any[]): GeoStylerFun * Get all child objects with a given tag name. * * @param elements An array of objects as created by the fast-xml-parser. - * @param tagName The tagname to get. + * @param tagName The tagName to get. * @returns An array of objects as created by the fast-xml-parser. */ export function getChildren(elements: any[], tagName: string): any[] { @@ -107,29 +105,17 @@ export function getChildren(elements: any[], tagName: string): any[] { } /** - * Get the child object with a given tag name. - * - * @param elements An array of objects as created by the fast-xml-parser. - * @param tagName The tagname to get. - * @returns An object as created by the fast-xml-parser. - */ -export function getChild(elements: any[], tagName: string): any { - return elements?.find(obj => Object.keys(obj).includes(tagName)); -} - -/** - * Get the value of a Css-/SvgParameter. + * Get the value of a parameter from a specific objects in a list of sld elements. * * @param elements An array of objects as created by the fast-xml-parser. + * @param paramKey The name of the parameter to find in the elements. * @param parameter The parameter name to get. - * @param sldVersion The sldVersion to distinguish if CssParameter or SvgParameter is used. * @returns The string value of the searched parameter. */ -export function getParameterValue(elements: any[], parameter: string, sldVersion: SldVersion): any { +export function getTextValueInSldObject(elements: any[], parameter: string, paramKey: string): any { if (!elements) { return undefined; } - const paramKey = sldVersion === '1.0.0' ? 'CssParameter' : 'SvgParameter'; const element = elements .filter(obj => Object.keys(obj)?.includes(paramKey)) .find(obj => obj?.[':@']?.['@_name'] === parameter); @@ -146,6 +132,30 @@ export function getParameterValue(elements: any[], parameter: string, sldVersion return element?.[paramKey]?.[0]?.['#text']; } +/** + * Get the value of a Css-/SvgParameter. + * + * @param elements An array of objects as created by the fast-xml-parser. + * @param parameter The parameter name to get. + * @param sldVersion The sldVersion to distinguish if CssParameter or SvgParameter is used. + * @returns The string value of the searched parameter. + */ +export function getParameterValue(elements: any[], parameter: string, sldVersion: SldVersion): any { + const paramKey = sldVersion === '1.0.0' ? 'CssParameter' : 'SvgParameter'; + return getTextValueInSldObject(elements, parameter, paramKey); +} + +/** + * Get the value of a (GeoServer) VendorOption. + * + * @param elements An array of objects as created by the fast-xml-parser. + * @param name The vendorOption name to get. + * @returns The string value of the searched parameter. + */ +export function getVendorOptionValue(elements: any[], name: string): any { + return getTextValueInSldObject(elements, name, 'VendorOption'); +} + /** * Get the attribute value of an object. * @@ -174,7 +184,7 @@ export function isSymbolizer(obj: any): boolean { * e.g. * Get text value: get(sldSymbolizer, 'Graphic.Mark.WellKnownName.#text') * Get an attribute value: get(sldSymbolizer, 'Graphic.ExternalGraphic.OnlineResource.@xlink:href') - * Get an Css-/SvgParameter value: get(sldSymbolizer, 'Graphic.Mark.Fill.$fill-opacity', '1.1.0') + * Get a Css-/SvgParameter value: get(sldSymbolizer, 'Graphic.Mark.Fill.$fill-opacity', '1.1.0') * Use with an index: get(sldObject, 'StyledLayerDescriptor.NamedLayer[1].UserStyle.Title.#text') * * @param obj A part of the parser result of the fast-xml-parser. @@ -240,3 +250,4 @@ export function get(obj: any, path: string, sldVersion?: SldVersion): any | unde export function keysByValue(object: any, value: any): string[] { return Object.keys(object).filter(key => object[key] === value); } +