diff --git a/data/mapbox/icontext_symbolizer.ts b/data/mapbox/icontext_symbolizer.ts new file mode 100644 index 0000000..4ff3423 --- /dev/null +++ b/data/mapbox/icontext_symbolizer.ts @@ -0,0 +1,25 @@ +import { MbStyle } from '../../src/MapboxStyleParser'; + +const iconTextSymbolizer: Omit = { + version: 8, + name: 'icontext symbolizer', + sprite: 'https://testurl.com', + layers: [ + { + type: 'symbol', + paint: { + 'text-color': 'rgba(45, 45, 45, 1)', + }, + layout: { + 'text-field': '{name}', + 'text-size': 12, + 'icon-image': 'poi', + visibility: 'visible', + }, + id: 'label and icon', + } + ] +}; + + +export default iconTextSymbolizer; diff --git a/data/mapbox_metadata/icontext_symbolizer.ts b/data/mapbox_metadata/icontext_symbolizer.ts new file mode 100644 index 0000000..7481fec --- /dev/null +++ b/data/mapbox_metadata/icontext_symbolizer.ts @@ -0,0 +1,38 @@ +import { MbStyle } from '../../src/MapboxStyleParser'; + +const iconTextSymbolizer: Omit = { + version: 8, + name: 'icontext symbolizer', + sprite: 'https://testurl.com', + layers: [ + { + type: 'symbol', + paint: { + 'text-color': 'rgba(45, 45, 45, 1)', + }, + layout: { + 'text-field': '{name}', + 'text-size': 12, + 'icon-image': 'poi', + visibility: 'visible', + }, + id: 'r0_sy0_st0', + } + ], + metadata: { + 'geostyler:ref': { + rules: [ + { + name: 'label and icon', + symbolizers: [ + [ + 'r0_sy0_st0', + ] + ], + }, + ], + }, + }, +}; + +export default iconTextSymbolizer; diff --git a/data/styles_metadata/icontext_symbolizer.ts b/data/styles_metadata/icontext_symbolizer.ts new file mode 100644 index 0000000..20f9f2e --- /dev/null +++ b/data/styles_metadata/icontext_symbolizer.ts @@ -0,0 +1,34 @@ +import { Style } from 'geostyler-style'; + +const iconTextSymbolizer: Style = { + name: 'icontext symbolizer', + rules: [ + { + name: 'label and icon', + symbolizers: [ + { + kind: 'Text', + color: 'rgba(45, 45, 45, 1)', + label: '{{name}}', + size: 12, + visibility: true + }, + { + kind: 'Icon', + visibility: true, + image: '/sprites/?name=poi&baseurl=' + encodeURIComponent('https://testurl.com') + } + ] + } + ], + metadata: { + 'mapbox:ref': { + splitSymbolizers: [{ + rule: 0, + symbolizers: [0, 1] + }] + } + } +}; + +export default iconTextSymbolizer; diff --git a/src/MapboxStyleParser.spec.ts b/src/MapboxStyleParser.spec.ts index 85924fc..2318734 100644 --- a/src/MapboxStyleParser.spec.ts +++ b/src/MapboxStyleParser.spec.ts @@ -48,6 +48,9 @@ import mb_line_patternline_metadata from '../data/mapbox_metadata/line_patternli import point_placeholdertext_simple from '../data/styles/point_placeholderText_simple'; import mb_point_placeholdertext_simple from '../data/mapbox/point_placeholderText_simple'; import mb_point_placeholdertext_simple_metadata from '../data/mapbox_metadata/point_placeholderText_simple'; +import icontext_symbolizer_metadata from '../data/styles_metadata/icontext_symbolizer'; +import mb_icontext_symbolizer from '../data/mapbox/icontext_symbolizer'; +import mb_icontext_symbolizer_metadata from '../data/mapbox_metadata/icontext_symbolizer'; import { CustomLayerInterface } from 'mapbox-gl'; import { AnyLayer } from 'mapbox-gl'; @@ -186,6 +189,12 @@ describe('MapboxStyleParser implements StyleParser', () => { expect(geoStylerStyle).toBeDefined(); expect(geoStylerStyle).toEqual(icon_simpleicon_mapboxapi); }); + + it('can read a mapbox style with icontext symbolizer', async () => { + const { output: geoStylerStyle } = await styleParser.readStyle(mb_icontext_symbolizer); + expect(geoStylerStyle).toBeDefined(); + expect(geoStylerStyle).toEqual(icontext_symbolizer_metadata); + }); }); describe('#writeStyle', () => { @@ -289,5 +298,11 @@ describe('MapboxStyleParser implements StyleParser', () => { expect(mbStyle).toBeDefined(); expect(mbStyle).toEqual(mb_icon_simpleicon_mapboxapi_metadata); }); + + it('can write a mapbox style with icontext symbolizer', async () => { + const { output: mbStyle } = await styleParser.writeStyle(icontext_symbolizer_metadata); + expect(mbStyle).toBeDefined(); + expect(mbStyle).toEqual(mb_icontext_symbolizer_metadata); + }); }); }); diff --git a/src/MapboxStyleParser.ts b/src/MapboxStyleParser.ts index fb12692..e37fc88 100644 --- a/src/MapboxStyleParser.ts +++ b/src/MapboxStyleParser.ts @@ -77,6 +77,13 @@ type GeoStylerRef = { }[]; }; +type MapboxRef = { + splitSymbolizers?: { + rule: number; + symbolizers: number[]; + }[]; +}; + type SymbolType = { textSymbolizer?: TextSymbolizer; iconSymbolizer?: IconSymbolizer; @@ -628,7 +635,7 @@ export class MapboxStyleParser implements StyleParser> merged.outlineJoin = lineSymbolizer.join; merged.outlineWidth = lineSymbolizer.width; } else { - throw new Error(`Trying to merge to symbolizers of differnt kinds: ${s1.kind}, ${s2.kind}`); + throw new Error(`Trying to merge two symbolizers of different kinds: ${s1.kind}, ${s2.kind}`); } } return merged; @@ -641,9 +648,10 @@ export class MapboxStyleParser implements StyleParser> * @param layer The mapbox Layer * @return A GeoStyler-Style Rule Array */ - mapboxLayersToGeoStylerRules(layers: NoneCustomLayer[]): Rule[] { + mapboxLayersToGeoStylerRules(layers: NoneCustomLayer[], mapboxRef: MapboxRef): Rule[] { const geoStylerRef: GeoStylerRef = this.mbMetadata?.['geostyler:ref']; const gsRules: Rule[] = []; + const splitSymbolizers: MapboxRef['splitSymbolizers'] = []; if (geoStylerRef) { geoStylerRef.rules.forEach((rule, ruleIndex) => { @@ -654,13 +662,22 @@ export class MapboxStyleParser implements StyleParser> rule.symbolizers?.forEach((layerIds, symbolizerIndex) => { const matchingLayers = layers.filter(layer => layerIds.includes(layer.id)); const flattenedSymbolizers = matchingLayers - .map(layer => this.getSymbolizersFromMapboxLayer(layer)) + .map(layer => { + const symbs = this.getSymbolizersFromMapboxLayer(layer); + if (symbs.length > 1) { + splitSymbolizers.push({ + rule: ruleIndex, + symbolizers: symbs.map((s, sIdx) => sIdx) + }); + } + return symbs; + }) .flat(); symbolizers[symbolizerIndex] = this.mergeSymbolizers(flattenedSymbolizers); // TODO: check if there are multiple layers with different filters - // and scaledenomintors and throw a warning that we only use the first + // and scaledenominators and throw a warning that we only use the first // one if (matchingLayers?.[0]) { filter = this.getFilterFromMapboxFilter(matchingLayers[0].filter); @@ -682,12 +699,11 @@ export class MapboxStyleParser implements StyleParser> }); } else { // returns array of rules where one rule contains one symbolizer - layers.forEach(layer => { + layers.forEach((layer, layerIdx) => { const symbolizers = this.getSymbolizersFromMapboxLayer(layer); if (symbolizers.length < 1) { return; } - const filter = this.getFilterFromMapboxFilter(layer.filter); const scaleDenominator = this.getScaleDenominatorFromMapboxZoom( layer.minzoom, @@ -699,11 +715,21 @@ export class MapboxStyleParser implements StyleParser> scaleDenominator, symbolizers }; - gsRules?.push(rule); + gsRules.push(rule); + if (symbolizers.length > 1) { + splitSymbolizers.push({ + rule: gsRules.length - 1, + symbolizers: symbolizers.map((s, sIdx) => sIdx) + }); + } }); } - return gsRules || []; + if (splitSymbolizers.length) { + mapboxRef.splitSymbolizers = splitSymbolizers; + } + + return gsRules; } /** @@ -716,6 +742,7 @@ export class MapboxStyleParser implements StyleParser> let style: Style = {} as Style; style.name = mapboxStyle.name || ''; style.rules = []; + const mapboxRef = {}; this.mbMetadata = mapboxStyle.metadata; if (mapboxStyle.sprite) { this.spriteBaseUrl = MapboxStyleUtil.getUrlForMbPlaceholder(mapboxStyle.sprite); @@ -725,9 +752,14 @@ export class MapboxStyleParser implements StyleParser> const layers = mapboxStyle.layers.filter( layer => !(layer.type === 'custom') ) as NoneCustomLayer[]; - const rules = this.mapboxLayersToGeoStylerRules(layers); + const rules = this.mapboxLayersToGeoStylerRules(layers, mapboxRef); style.rules = style.rules.concat(rules); + } + if (Object.keys(mapboxRef).length) { + style.metadata = { + 'mapbox:ref': mapboxRef + }; } return style; } @@ -791,7 +823,8 @@ export class MapboxStyleParser implements StyleParser> // Mapbox Style version const version = 8; const name = geoStylerStyle.name; - const {layers, geoStylerRef} = this.getMapboxLayersFromRules(geoStylerStyle.rules); + const metadata = geoStylerStyle.metadata?.['mapbox:ref']; + const {layers, geoStylerRef} = this.getMapboxLayersFromRules(geoStylerStyle.rules, metadata); const sprite = MapboxStyleUtil.getMbPlaceholderForUrl(this.spriteBaseUrl); let mapboxObject = omitBy({ @@ -819,7 +852,7 @@ export class MapboxStyleParser implements StyleParser> * @param rules An array of GeoStylerStyle-Rules * @return An array of Mapbox Layers */ - getMapboxLayersFromRules(rules: Rule[]): {layers: NoneCustomLayer[]; geoStylerRef: GeoStylerRef} + getMapboxLayersFromRules(rules: Rule[], mapboxRef: MapboxRef): {layers: NoneCustomLayer[]; geoStylerRef: GeoStylerRef} { // one layer corresponds to a single symbolizer within a rule // so filters and scaleDenominators have to be set for each symbolizer explicitly @@ -852,11 +885,12 @@ export class MapboxStyleParser implements StyleParser> } } - rule.symbolizers.forEach((symbolizer: Symbolizer, symbolizerIndex: number) => { - geoStylerRef.rules[ruleIndex] = { - name: rule.name - }; + geoStylerRef.rules[ruleIndex] = { + name: rule.name, + symbolizers: [] + }; + rule.symbolizers.forEach((symbolizer: Symbolizer, symbolizerIndex: number) => { // use existing layer properties let lyr: any = {}; lyr.filter = layer.filter; @@ -866,6 +900,10 @@ export class MapboxStyleParser implements StyleParser> const styles = this.getStyleFromSymbolizer(symbolizer); + const shouldMergeIntoPrev = mapboxRef?.splitSymbolizers?.some( + s => s.rule === ruleIndex && s.symbolizers.length > 1 && s.symbolizers.slice(1).includes(symbolizerIndex) + ); + styles.forEach((style: any, styleIndex: number) => { const { type, paint, layout @@ -873,6 +911,13 @@ export class MapboxStyleParser implements StyleParser> let lyrClone = structuredClone(lyr); + if (shouldMergeIntoPrev) { + lyrClone = layers.at(-1); + lyrClone.paint = {...lyrClone.paint, ...paint}; + lyrClone.layout = {...lyrClone.layout, ...layout}; + return; + } + lyrClone.type = type; if (!MapboxStyleUtil.allUndefined(paint)) { lyrClone.paint = paint; @@ -884,10 +929,17 @@ export class MapboxStyleParser implements StyleParser> layers.push(omitBy(lyrClone, isUndefined) as NoneCustomLayer); - if (!Array.isArray(geoStylerRef?.rules?.[ruleIndex]?.symbolizers)) { - geoStylerRef.rules[ruleIndex].symbolizers = [[]]; + let symbs = geoStylerRef.rules[ruleIndex].symbolizers; + if (!symbs) { + symbs = []; } - geoStylerRef.rules[ruleIndex]?.symbolizers?.[symbolizerIndex].push(lyrClone.id); + + if (!symbs[symbolizerIndex]) { + symbs[symbolizerIndex] = []; + } + + symbs[symbolizerIndex].push(lyrClone.id); + geoStylerRef.rules[ruleIndex].symbolizers = symbs; }); }); }); diff --git a/src/Util/MapboxStyleUtil.ts b/src/Util/MapboxStyleUtil.ts index a71899b..0681fb2 100644 --- a/src/Util/MapboxStyleUtil.ts +++ b/src/Util/MapboxStyleUtil.ts @@ -108,7 +108,7 @@ class MapboxStyleUtil { // TODO: TextSymbolizer can be removed once it is fixed in the geostyler-style public static symbolizerAllUndefined(symbolizer: Symbolizer | TextSymbolizer): boolean { return !Object.keys(symbolizer) - .filter(val => val !== 'kind') + .filter(val => val !== 'kind' && val !== 'visibility') .some((val: keyof Symbolizer) => typeof symbolizer[val] !== 'undefined'); }