Skip to content

Commit 3fceafb

Browse files
committed
feat(LineSymbolizer): support graphicStroke incl. dasharray
1 parent a216157 commit 3fceafb

9 files changed

Lines changed: 1329 additions & 20 deletions

data/styles/line_graphicStroke.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Style } from 'geostyler-style';
2+
3+
const lineSimpleLine: Style = {
4+
name: 'OL Style',
5+
rules: [
6+
{
7+
name: 'OL Style Rule 0',
8+
symbolizers: [{
9+
kind: 'Line',
10+
graphicStroke: {
11+
kind: 'Mark',
12+
wellKnownName: 'square',
13+
color: '#000000',
14+
radius: 10
15+
}
16+
}]
17+
}
18+
]
19+
};
20+
21+
export default lineSimpleLine;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Style } from 'geostyler-style';
2+
3+
const lineSimpleLine: Style = {
4+
name: 'OL Style',
5+
rules: [
6+
{
7+
name: 'OL Style Rule 0',
8+
symbolizers: [{
9+
kind: 'Line',
10+
graphicStroke: {
11+
kind: 'Mark',
12+
wellKnownName: 'square',
13+
color: '#000000',
14+
radius: 5
15+
},
16+
dasharray: [20, 10]
17+
}]
18+
}
19+
]
20+
};
21+
22+
export default lineSimpleLine;

src/OlStyleParser.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import OlStyleIcon from 'ol/style/Icon';
77
import OlStyleText, { Options as TextOptions } from 'ol/style/Text';
88
import OlStyleFill, { Options as FillOptions } from 'ol/style/Fill';
99
import OlFeature from 'ol/Feature';
10+
import OlLineString from 'ol/geom/LineString';
11+
import OlPoint from 'ol/geom/Point';
1012

1113
import OlStyleParser, { OlParserStyleFct } from './OlStyleParser';
1214

@@ -31,6 +33,8 @@ import point_simpledot from '../data/styles/point_simpledot';
3133
import point_simpleplus from '../data/styles/point_simpleplus';
3234
import point_simpletimes from '../data/styles/point_simpletimes';
3335
import line_simpleline from '../data/styles/line_simpleline';
36+
import line_graphicstroke from '../data/styles/line_graphicStroke';
37+
import line_graphicstroke_dasharray from '../data/styles/line_graphicStrokeDashArray';
3438
import filter_simplefilter from '../data/styles/filter_simpleFilter';
3539
import filter_nestedfilter from '../data/styles/filter_nestedFilter';
3640
import filter_invalidfilter from '../data/styles/filter_invalidFilter';
@@ -856,6 +860,52 @@ describe('OlStyleParser implements StyleParser', () => {
856860
expect(olStroke?.getWidth()).toBeCloseTo(expecSymb.width as number);
857861
expect(olStroke?.getLineDash()).toEqual(expecSymb.dasharray);
858862
});
863+
it('can write an OpenLayers LineSymbolizer with graphicStroke', async () => {
864+
let { output: olStyle } = await styleParser.writeStyle(line_graphicstroke);
865+
olStyle = olStyle as OlStyle;
866+
expect(olStyle).toBeDefined();
867+
868+
const testFeature = new OlFeature({
869+
geometry: new OlLineString([[0, 0], [0, 20]])
870+
});
871+
const resolution = 1;
872+
const expecSymb = (line_graphicstroke.rules[0].symbolizers[0] as LineSymbolizer).graphicStroke as MarkSymbolizer;
873+
const evaluatedStyle = (olStyle as unknown as OlParserStyleFct)(testFeature, resolution)[0];
874+
const geometry = evaluatedStyle.getGeometry();
875+
expect(geometry).toBeInstanceOf(OlPoint);
876+
877+
const iconStyle = evaluatedStyle.getImage();
878+
const svgString = getDecodedSvg(iconStyle.getSrc() as string);
879+
const { wellKnownName, color } = getSvgProperties(svgString) as MarkSymbolizer;
880+
881+
expect(wellKnownName).toEqual(cleanWellKnownName(expecSymb.wellKnownName));
882+
expect(color).toEqual(expecSymb.color);
883+
});
884+
it('can write an OpenLayers LineSymbolizer with graphicStroke with dash array', async () => {
885+
let { output: olStyle } = await styleParser.writeStyle(line_graphicstroke_dasharray);
886+
olStyle = olStyle as OlStyle;
887+
expect(olStyle).toBeDefined();
888+
889+
const testFeature = new OlFeature({
890+
geometry: new OlLineString([[0, 0], [0, 60]])
891+
});
892+
const resolution = 1;
893+
const expecSymb = (line_graphicstroke_dasharray.rules[0].symbolizers[0] as LineSymbolizer).graphicStroke as MarkSymbolizer;
894+
const evaluatedStyle = (olStyle as unknown as OlParserStyleFct)(testFeature, resolution);
895+
// 4 symbols fit into dash pattern
896+
expect(evaluatedStyle).toHaveLength(4);
897+
898+
const coordsGeom1 = evaluatedStyle[0].getGeometry().getCoordinates();
899+
const coordsGeom2 = evaluatedStyle[1].getGeometry().getCoordinates();
900+
const coordsGeom3 = evaluatedStyle[2].getGeometry().getCoordinates();
901+
const coordsGeom4 = evaluatedStyle[3].getGeometry().getCoordinates();
902+
903+
// for all geometries the first ordinate is always 0, so we only check the second.
904+
expect(coordsGeom1[1]).toBeCloseTo(5);
905+
expect(coordsGeom2[1]).toBeCloseTo(15);
906+
expect(coordsGeom3[1]).toBeCloseTo(35);
907+
expect(coordsGeom4[1]).toBeCloseTo(45);
908+
});
859909
it('can write an OpenLayers PolygonSymbolizer', async () => {
860910
let { output: olStyle } = await styleParser.writeStyle(polygon_transparentpolygon);
861911
olStyle = olStyle as OlStyle;

src/OlStyleParser.ts

Lines changed: 145 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import OlStyleCircle from 'ol/style/Circle';
3838
import OlStyleFill from 'ol/style/Fill';
3939
import OlStyleIcon, { Options as OlStyleIconOptions } from 'ol/style/Icon';
4040
import OlStyleRegularshape from 'ol/style/RegularShape';
41+
import OlLineString from 'ol/geom/LineString';
42+
import OlMultiLineString from 'ol/geom/MultiLineString';
4143
import { METERS_PER_UNIT } from 'ol/proj/Units';
4244

4345
import OlStyleUtil, { DEGREES_TO_RADIANS } from './Util/OlStyleUtil';
@@ -53,6 +55,7 @@ import {
5355
LINE_WELLKNOWNNAMES,
5456
NOFILL_WELLKNOWNNAMES
5557
} from './Util/OlSvgUtil';
58+
import OlGraphicStrokeUtil from './Util/OlGraphicStrokeUtil';
5659

5760
export interface OlParserStyleFct {
5861
(feature?: any, resolution?: number): any;
@@ -167,6 +170,9 @@ export class OlStyleParser implements StyleParser<OlStyleLike> {
167170
OlStyleCircleConstructor = OlStyleCircle;
168171
OlStyleIconConstructor = OlStyleIcon;
169172
OlStyleRegularshapeConstructor = OlStyleRegularshape;
173+
OlLineStringContructor = OlLineString;
174+
OlMultiLineStringConstructor = OlMultiLineString;
175+
OlPointConstructor = OlGeomPoint;
170176

171177
constructor(ol?: any) {
172178
if (ol) {
@@ -178,6 +184,9 @@ export class OlStyleParser implements StyleParser<OlStyleLike> {
178184
this.OlStyleCircleConstructor = ol.style.Circle;
179185
this.OlStyleIconConstructor = ol.style.Icon;
180186
this.OlStyleRegularshapeConstructor = ol.style.RegularShape;
187+
this.OlLineStringContructor = ol.geom.LineString;
188+
this.OlMultiLineStringConstructor = ol.geom.MultiLineString;
189+
this.OlPointConstructor = ol.geom.Point;
181190
}
182191
}
183192

@@ -749,6 +758,7 @@ export class OlStyleParser implements StyleParser<OlStyleLike> {
749758
const hasMaxScale = geoStylerStyle?.rules?.[0]?.scaleDenominator?.max !== undefined ? true : false;
750759
const hasScaleDenominator = hasMinScale || hasMaxScale ? true : false;
751760
const hasFunctions = OlStyleUtil.containsGeoStylerFunctions(geoStylerStyle);
761+
const hasGraphicStroke = OlGraphicStrokeUtil.containsGraphicStroke(geoStylerStyle);
752762

753763
const nrSymbolizers = geoStylerStyle.rules[0].symbolizers.length;
754764
const hasTextSymbolizer = rules[0].symbolizers.some((symbolizer: Symbolizer) => {
@@ -757,7 +767,14 @@ export class OlStyleParser implements StyleParser<OlStyleLike> {
757767
const hasDynamicIconSymbolizer = rules[0].symbolizers.some((symbolizer: Symbolizer) => {
758768
return symbolizer.kind === 'Icon' && typeof(symbolizer.image) === 'string' && symbolizer.image.includes('{{');
759769
});
760-
if (!hasFilter && !hasScaleDenominator && !hasTextSymbolizer && !hasDynamicIconSymbolizer && !hasFunctions) {
770+
if (
771+
!hasFilter
772+
&& !hasScaleDenominator
773+
&& !hasTextSymbolizer
774+
&& !hasDynamicIconSymbolizer
775+
&& !hasFunctions
776+
&& !hasGraphicStroke
777+
) {
761778
if (nrSymbolizers === 1) {
762779
return this.geoStylerStyleToOlStyle(geoStylerStyle);
763780
} else {
@@ -848,7 +865,9 @@ export class OlStyleParser implements StyleParser<OlStyleLike> {
848865
if (isWithinScale && matchesFilter) {
849866
rule.symbolizers.forEach((symb: Symbolizer) => {
850867
if (symb.visibility === false) {
868+
// TODO why pushing null instead of just skipping/returning?
851869
styles.push(null);
870+
return;
852871
}
853872

854873
if (isGeoStylerBooleanFunction(symb.visibility)) {
@@ -858,16 +877,25 @@ export class OlStyleParser implements StyleParser<OlStyleLike> {
858877
}
859878
}
860879

861-
const olSymbolizer: any = this.getOlSymbolizerFromSymbolizer(symb, feature);
862-
// either an OlStyle or an ol.StyleFunction. OpenLayers only accepts an array
880+
const olSymbolizer: any = this.getOlSymbolizerFromSymbolizer(symb, feature, resolution);
881+
// either an OlStyle, OlStyle[] or an ol.StyleFunction. OpenLayers only accepts an array
863882
// of OlStyles, not ol.StyleFunctions.
864883
// So we have to check it and in case of an ol.StyleFunction call that function
865884
// and add the returned style to const styles.
866-
if (typeof olSymbolizer !== 'function') {
867-
styles.push(olSymbolizer);
868-
} else {
885+
if (typeof olSymbolizer === 'function') {
869886
const styleFromFct: any = olSymbolizer(feature, resolution);
870887
styles.push(styleFromFct);
888+
} else if (Array.isArray(olSymbolizer)) {
889+
olSymbolizer.forEach((s) => {
890+
if (typeof s === 'function') {
891+
const styleFromFct: any = s(feature, resolution);
892+
styles.push(styleFromFct);
893+
} else {
894+
styles.push(s);
895+
}
896+
});
897+
} else {
898+
styles.push(olSymbolizer);
871899
}
872900
});
873901
}
@@ -995,7 +1023,7 @@ export class OlStyleParser implements StyleParser<OlStyleLike> {
9951023
* @param symbolizer A GeoStyler-Style Symbolizer.
9961024
* @return The OpenLayers Style object or a StyleFunction
9971025
*/
998-
getOlSymbolizerFromSymbolizer(symbolizer: Symbolizer, feature?: OlFeature): OlStyle {
1026+
getOlSymbolizerFromSymbolizer(symbolizer: Symbolizer, feature?: OlFeature, resolution?: number): OlStyle {
9991027
let olSymbolizer: any;
10001028
symbolizer = structuredClone(symbolizer);
10011029

@@ -1010,7 +1038,7 @@ export class OlStyleParser implements StyleParser<OlStyleLike> {
10101038
olSymbolizer = this.getOlTextSymbolizerFromTextSymbolizer(symbolizer, feature);
10111039
break;
10121040
case 'Line':
1013-
olSymbolizer = this.getOlLineSymbolizerFromLineSymbolizer(symbolizer, feature);
1041+
olSymbolizer = this.getOlLineSymbolizerFromLineSymbolizer(symbolizer, feature, resolution);
10141042
break;
10151043
case 'Fill':
10161044
olSymbolizer = this.getOlPolygonSymbolizerFromFillSymbolizer(symbolizer, feature);
@@ -1195,7 +1223,15 @@ export class OlStyleParser implements StyleParser<OlStyleLike> {
11951223
* @param symbolizer A GeoStyler-Style LineSymbolizer.
11961224
* @return The OL Style object
11971225
*/
1198-
getOlLineSymbolizerFromLineSymbolizer(symbolizer: LineSymbolizer, feat?: OlFeature): OlStyle | OlStyleStroke {
1226+
getOlLineSymbolizerFromLineSymbolizer(
1227+
symbolizer: LineSymbolizer, feat?: OlFeature, resolution?: number
1228+
): OlStyle | OlStyleStroke | OlStyle[] {
1229+
// graphicStroke will always be evaluated in a function, so we
1230+
// can assume that the feature is available.
1231+
if (symbolizer.graphicStroke) {
1232+
// If graphicStroke is set, we ignore unrelated stroke properties
1233+
return this.getOlGraphicStrokeFromGraphicStroke(symbolizer, feat!, resolution!);
1234+
}
11991235
for (const key of Object.keys(symbolizer)) {
12001236
if (isGeoStylerFunction(symbolizer[key as keyof LineSymbolizer])) {
12011237
(symbolizer as any)[key] = OlStyleUtil.evaluateFunction((symbolizer as any)[key], feat);
@@ -1218,6 +1254,106 @@ export class OlStyleParser implements StyleParser<OlStyleLike> {
12181254
});
12191255
}
12201256

1257+
getOlGraphicStrokeFromGraphicStroke(
1258+
symbolizer: LineSymbolizer, feat: OlFeature, resolution: number
1259+
) {
1260+
const geom = feat.getGeometry();
1261+
if (!geom || !(geom instanceof this.OlLineStringContructor || geom instanceof this.OlMultiLineStringConstructor)) {
1262+
throw new Error(
1263+
'GraphicStroke can only be applied to (Multi-)LineString geometries'
1264+
);
1265+
}
1266+
1267+
const graphicStroke = symbolizer.graphicStroke!;
1268+
const symbolSize = this.getSymbolSizeFromGraphicStroke(graphicStroke, feat);
1269+
if (symbolSize <= 0) {
1270+
console.warn('Symbol size must be greater than zero for graphic stroke. No graphic will be drawn.');
1271+
return [];
1272+
}
1273+
const symbolRotation = graphicStroke.rotate;
1274+
const evaluatedSymbolRotation = isGeoStylerFunction(symbolRotation)
1275+
? OlStyleUtil.evaluateNumberFunction(symbolRotation, feat)
1276+
: symbolRotation ?? 0;
1277+
// We currently do not support expressions for dasharrays
1278+
const dashArray = symbolizer.dasharray as number[] | undefined;
1279+
const dashOffset = symbolizer.dashOffset || 0;
1280+
const evaluatedDashOffset = isGeoStylerFunction(dashOffset)
1281+
? OlStyleUtil.evaluateNumberFunction(dashOffset, feat)
1282+
: dashOffset;
1283+
1284+
const symbolizerGenerator = (modifiedGraphicStroke: any) => {
1285+
return this.getOlSymbolizerFromSymbolizer(modifiedGraphicStroke, feat, resolution);
1286+
};
1287+
1288+
if (geom instanceof this.OlLineStringContructor) {
1289+
return OlGraphicStrokeUtil.processLineStringGraphicStroke(
1290+
geom,
1291+
symbolSize,
1292+
resolution,
1293+
dashArray,
1294+
evaluatedDashOffset,
1295+
evaluatedSymbolRotation,
1296+
graphicStroke,
1297+
symbolizerGenerator,
1298+
this.OlPointConstructor
1299+
);
1300+
} else {
1301+
// For every line in the MultiLineString, we start the pattern at the
1302+
// beginning of the line. This means that the pattern can be
1303+
// discontinuous at vertices where the lines connect.
1304+
return geom.getLineStrings().flatMap(line =>
1305+
OlGraphicStrokeUtil.processLineStringGraphicStroke(
1306+
line,
1307+
symbolSize,
1308+
resolution,
1309+
dashArray,
1310+
evaluatedDashOffset,
1311+
evaluatedSymbolRotation,
1312+
graphicStroke,
1313+
symbolizerGenerator,
1314+
this.OlPointConstructor
1315+
)
1316+
);
1317+
}
1318+
}
1319+
1320+
/**
1321+
* Get the size of a symbol from graphicStroke.
1322+
* @param graphicStroke The graphicStroke from the LineSymbolizer.
1323+
* @param feat The feature.
1324+
* @returns The size of the symbol in pixels.
1325+
*/
1326+
getSymbolSizeFromGraphicStroke(
1327+
graphicStroke: LineSymbolizer['graphicStroke'], feat: OlFeature
1328+
) {
1329+
let size = 0;
1330+
if (graphicStroke!.kind === 'Mark') {
1331+
const radius = graphicStroke!.radius;
1332+
if (isGeoStylerFunction(radius)) {
1333+
size = OlStyleUtil.evaluateNumberFunction(radius, feat) * 2;
1334+
} else {
1335+
size = (radius ?? 0) * 2;
1336+
}
1337+
if (size <= 0) {
1338+
return size;
1339+
}
1340+
const strokeWidth = graphicStroke!.strokeWidth;
1341+
if (isGeoStylerFunction(strokeWidth)) {
1342+
size += OlStyleUtil.evaluateNumberFunction(strokeWidth, feat);
1343+
} else {
1344+
size += strokeWidth ?? 0;
1345+
}
1346+
} else if (graphicStroke!.kind === 'Icon') {
1347+
const iconSize = graphicStroke!.size;
1348+
if (isGeoStylerFunction(iconSize)) {
1349+
size = OlStyleUtil.evaluateNumberFunction(iconSize, feat);
1350+
} else {
1351+
size = iconSize ?? 0;
1352+
}
1353+
}
1354+
return size;
1355+
}
1356+
12211357
/**
12221358
* Get the OL Style object from an GeoStyler-Style FillSymbolizer.
12231359
*

0 commit comments

Comments
 (0)