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);
}
+