diff --git a/data/mapbox/color_rgba.ts b/data/mapbox/color_rgba.ts new file mode 100644 index 0000000..ed43afb --- /dev/null +++ b/data/mapbox/color_rgba.ts @@ -0,0 +1,30 @@ +import { MbStyle } from '../../src/MapboxStyleParser'; + +const circleSimpleCircle: MbStyle = { + version: 8, + name: 'Simple Circle', + sources: { + testsource: { + type: 'vector' + } + }, + layers: [ + { + id: 'Simple Circle', + source: 'testsource', + 'source-layer': 'foo', + type: 'circle', + paint: { + 'circle-color': 'rgba(0, 0, 0, 1)', + 'circle-stroke-color': [ + 'case', + ['<', ['get', 'mag'], 2], + 'rgba(255, 0, 0, 1)', + 'rgba(0, 255, 0, 1)' + ] + } + } + ] +}; + +export default circleSimpleCircle; diff --git a/data/mapbox_metadata/icontext_symbolizer.ts b/data/mapbox_metadata/icontext_symbolizer.ts index affa6e9..1078c54 100644 --- a/data/mapbox_metadata/icontext_symbolizer.ts +++ b/data/mapbox_metadata/icontext_symbolizer.ts @@ -15,7 +15,7 @@ const iconTextSymbolizer: MbStyle = { source: 'testsource', 'source-layer': 'foo', paint: { - 'text-color': 'rgba(45, 45, 45, 1)', + 'text-color': '#2d2d2d', }, layout: { 'text-field': '{name}', diff --git a/data/styles/color_rgba.ts b/data/styles/color_rgba.ts new file mode 100644 index 0000000..cc7ab1d --- /dev/null +++ b/data/styles/color_rgba.ts @@ -0,0 +1,45 @@ +import { Style } from 'geostyler-style'; + +const circleSimpleCircle: Style = { + name: 'Simple Circle', + rules: [{ + name: 'Simple Circle', + symbolizers: [{ + kind: 'Mark', + wellKnownName: 'circle', + color: '#000000', + strokeColor: { + name: 'case', + args: [{ + case: { + name: 'lessThan', + args: [{ + name: 'property', + args: ['mag'] + }, 2] + }, + value: '#ff0000' + }, + '#00ff00' + ] + } + }] + }], + metadata: { + 'mapbox:ref': { + sources: { + testsource: { + type: 'vector' + } + }, + sourceMapping: { + testsource: [0] + }, + sourceLayerMapping: { + foo: [0] + } + } + } +}; + +export default circleSimpleCircle; diff --git a/data/styles_metadata/icontext_symbolizer.ts b/data/styles_metadata/icontext_symbolizer.ts index e69ab44..40a2238 100644 --- a/data/styles_metadata/icontext_symbolizer.ts +++ b/data/styles_metadata/icontext_symbolizer.ts @@ -8,7 +8,7 @@ const iconTextSymbolizer: Style = { symbolizers: [ { kind: 'Text', - color: 'rgba(45, 45, 45, 1)', + color: '#2d2d2d', label: '{{name}}', size: 12, visibility: true diff --git a/src/Expressions.ts b/src/Expressions.ts index 800dcdf..c46e3fe 100644 --- a/src/Expressions.ts +++ b/src/Expressions.ts @@ -13,6 +13,8 @@ import { StyleFunction } from 'mapbox-gl'; +import MapboxStyleUtil from './Util/MapboxStyleUtil'; + const expressionNames: ExpressionName[] = ['array', 'boolean', 'collator', @@ -216,12 +218,15 @@ export function gs2mbExpression(gsExpression?: GeoStyler type MbInput = MapboxExpression | PropertyType | StyleFunction; -export function mb2gsExpression(mbExpression?: MbInput): +export function mb2gsExpression(mbExpression?: MbInput, isColor?: boolean): GeoStylerExpression | undefined { // TODO: is this check valid ? // fails for arrays like offset = [10, 20] if (!(Array.isArray(mbExpression) && expressionNames.includes(mbExpression[0]))) { + if (typeof mbExpression === 'string' && isColor) { + return MapboxStyleUtil.getHexColor(mbExpression) as GeoStylerExpression; + } return mbExpression as GeoStylerExpression | undefined; } @@ -240,7 +245,7 @@ export function mb2gsExpression(mbExpression?: MbInput): case 'case': const gsArgs: any[] = []; args.forEach((a, index) => { - if (index < (args.length - 1)) { + if (index < (args.length - 1)) { var gsIndex = index < 2 ? 0 : Math.floor(index / 2); if (!gsArgs[gsIndex]) { gsArgs[gsIndex] = {}; @@ -252,12 +257,12 @@ export function mb2gsExpression(mbExpression?: MbInput): } else { gsArgs[gsIndex] = { ...gsArgs[gsIndex] as any, - value: mb2gsExpression(a) + value: mb2gsExpression(a, isColor) }; } } }); - gsArgs.push(mbExpression.at(- 1)); + gsArgs.push(mb2gsExpression(mbExpression.at(-1), isColor)); func = { name: 'case', args: gsArgs as Fcase['args'] @@ -275,7 +280,7 @@ export function mb2gsExpression(mbExpression?: MbInput): } func = { name: gsFunctionName, - args: args.map(mb2gsExpression) + args: args.map(arg => mb2gsExpression(arg)) } as GeoStylerFunction; break; } diff --git a/src/MapboxStyleParser.spec.ts b/src/MapboxStyleParser.spec.ts index 9033582..6b13123 100644 --- a/src/MapboxStyleParser.spec.ts +++ b/src/MapboxStyleParser.spec.ts @@ -68,6 +68,8 @@ import mb_text_placement_line_metadata from '../data/mapbox_metadata/text_placem import mb_text_placement_point from '../data/mapbox/text_placement_point'; import text_placement_point from '../data/styles/text_placement_point'; import mb_text_placement_point_metadata from '../data/mapbox_metadata/text_placement_point'; +import color_rgba from '../data/styles/color_rgba'; +import mb_color_rgba from '../data/mapbox/color_rgba'; const mockFetchResult = (data: any) => { jest.spyOn(global, 'fetch') @@ -287,6 +289,12 @@ describe('MapboxStyleParser implements StyleParser', () => { expect(geostylerStyle).toBeDefined(); expect(geostylerStyle).toEqual(source_layer_mapping); }); + + it('can read a style with rgba color values', async () => { + const { output: geostylerStyle } = await styleParser.readStyle(mb_color_rgba); + expect(geostylerStyle).toBeDefined(); + expect(geostylerStyle).toEqual(color_rgba); + }); }); describe('#writeStyle', () => { diff --git a/src/MapboxStyleParser.ts b/src/MapboxStyleParser.ts index 822c81a..e3e914e 100644 --- a/src/MapboxStyleParser.ts +++ b/src/MapboxStyleParser.ts @@ -369,7 +369,7 @@ export class MapboxStyleParser implements StyleParser> const symbolizer: MarkSymbolizer = { kind: 'Mark', blur: mb2gsExpression(paint?.['circle-blur']), - color: mb2gsExpression(paint?.['circle-color']), + color: mb2gsExpression(paint?.['circle-color'], true), // TODO: handle array values offset: paint?.['circle-translate'] as MarkSymbolizer['offset'], offsetAnchor: paint?.['circle-translate-anchor'], @@ -377,7 +377,7 @@ export class MapboxStyleParser implements StyleParser> pitchAlignment: paint?.['circle-pitch-alignment'], pitchScale: paint?.['circle-pitch-scale'], radius: mb2gsExpression(paint?.['circle-radius']), - strokeColor: mb2gsExpression(paint?.['circle-stroke-color']), + strokeColor: mb2gsExpression(paint?.['circle-stroke-color'], true), strokeOpacity: mb2gsExpression(paint?.['circle-stroke-opacity']), strokeWidth: mb2gsExpression(paint?.['circle-stroke-width']), visibility: layout?.visibility && layout?.visibility !== 'none', @@ -409,9 +409,9 @@ export class MapboxStyleParser implements StyleParser> allowOverlap: mb2gsExpression(layout?.['icon-allow-overlap']), anchor: layout?.['icon-anchor'] as IconSymbolizer['anchor'], avoidEdges: layout?.['symbol-avoid-edges'], - color: mb2gsExpression(paint?.['icon-color']), + color: mb2gsExpression(paint?.['icon-color'], true), haloBlur: mb2gsExpression(paint?.['icon-halo-blur']), - haloColor: mb2gsExpression(paint?.['icon-halo-color']), + haloColor: mb2gsExpression(paint?.['icon-halo-color'], true), haloWidth: mb2gsExpression(paint?.['icon-halo-width']), image, keepUpright: mb2gsExpression(layout?.['icon-keep-upright']), @@ -476,11 +476,11 @@ export class MapboxStyleParser implements StyleParser> // TODO: handle enum values anchor: layout?.['text-anchor'] as TextSymbolizer['anchor'], avoidEdges: mb2gsExpression(layout?.['symbol-avoid-edges']), - color: mb2gsExpression(paint?.['text-color']), + color: mb2gsExpression(paint?.['text-color'], true), // TODO: handle array values font: layout?.['text-font'], haloBlur: mb2gsExpression(paint?.['text-halo-blur']), - haloColor: mb2gsExpression(paint?.['text-halo-color']), + haloColor: mb2gsExpression(paint?.['text-halo-color'], true), haloWidth: mb2gsExpression(paint?.['text-halo-width']), // TODO: handle enum values justify: layout?.['text-justify'] as TextSymbolizer['justify'], @@ -530,8 +530,8 @@ export class MapboxStyleParser implements StyleParser> visibility: layout?.visibility && layout?.visibility !== 'none', antialias: mb2gsExpression(paint?.['fill-antialias']), opacity: mb2gsExpression(paint?.['fill-opacity']), - color: mb2gsExpression(paint?.['fill-color']), - outlineColor: mb2gsExpression(paint?.['fill-outline-color']), + color: mb2gsExpression(paint?.['fill-color'], true), + outlineColor: mb2gsExpression(paint?.['fill-outline-color'], true), graphicFill }; return omitBy(fillSymbolizer, isUndefined) as FillSymbolizer; @@ -566,7 +566,7 @@ export class MapboxStyleParser implements StyleParser> miterLimit: mb2gsExpression(layout?.['line-miter-limit']), roundLimit: mb2gsExpression(layout?.['line-round-limit']), opacity: mb2gsExpression(paint?.['line-opacity']), - color: mb2gsExpression(paint?.['line-color']), + color: mb2gsExpression(paint?.['line-color'], true), width: mb2gsExpression(paint?.['line-width']), gapWidth: mb2gsExpression(paint?.['line-gap-width']), perpendicularOffset: mb2gsExpression(paint?.['line-offset']), diff --git a/src/Util/MapboxStyleUtil.ts b/src/Util/MapboxStyleUtil.ts index ddbfb7d..536c9a0 100644 --- a/src/Util/MapboxStyleUtil.ts +++ b/src/Util/MapboxStyleUtil.ts @@ -177,6 +177,59 @@ class MapboxStyleUtil { } return name || ''; } + + /** + * Splits a RGBA encoded color into its color values. + * + * @param {string} rgbaColor RGB(A) encoded color + * @return {number[]} Numeric color values as array + */ + public static splitRgbaColor(rgbaColor: string): number[] { + const colorsOnly = rgbaColor.substring(rgbaColor.indexOf('(') + 1, rgbaColor.lastIndexOf(')')).split(/,\s*/); + const red = parseInt(colorsOnly[0], 10); + const green = parseInt(colorsOnly[1], 10); + const blue = parseInt(colorsOnly[2], 10); + const opacity = parseFloat(colorsOnly[3]); + + return [red, green, blue, opacity]; + } + + /** + * Returns the hex code for a given RGB(A) array. + * + * @param colorArr RGB(A) array. e.g. [255,0,0] + * @return {string} The HEX color representation of the given color + */ + public static getHexCodeFromRgbArray(colorArr: number[]): string { + return '#' + colorArr.map((x, idx) => { + const hex = x.toString(16); + // skip opacity if passed as fourth entry + if (idx < 3) { + return hex.length === 1 ? '0' + hex : hex; + } + return ''; + }).join(''); + } + + /** + * Transforms a RGB(A) or named color value to a HEX encoded notation. + * If a HEX color is provided it will be returned untransformed. + * + * @param {string} inColor The color to transform + * @return {string | undefined} The HEX color representation of the given color + */ + public static getHexColor(inColor: string): string | undefined { + // if passing in a hex code we just return it + if (inColor.startsWith('#')) { + return inColor; + } else if (inColor.startsWith('rgb')) { + const colorArr = this.splitRgbaColor(inColor); + return this.getHexCodeFromRgbArray(colorArr); + } else { + return; + } + } + } export default MapboxStyleUtil;