diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a724275c..1e11eaec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## main ### ✨ Features and improvements +- Add `vertical-align` option to `format` expression ([#832](https://github.com/maplibre/maplibre-style-spec/issues/832)) - _...Add new stuff here..._ ### 🐞 Bug fixes diff --git a/src/expression/definitions/format.ts b/src/expression/definitions/format.ts index 1d340c0ad..038813fb2 100644 --- a/src/expression/definitions/format.ts +++ b/src/expression/definitions/format.ts @@ -7,7 +7,7 @@ import { ColorType, ResolvedImageType, } from '../types'; -import {Formatted, FormattedSection} from '../types/formatted'; +import {Formatted, FormattedSection, VERTICAL_ALIGN_OPTIONS, VerticalAlign} from '../types/formatted'; import {valueToString, typeOf} from '../values'; import type {Expression} from '../expression'; @@ -22,6 +22,7 @@ type FormattedSectionExpression = { scale: Expression | null; font: Expression | null; textColor: Expression | null; + verticalAlign: Expression | null; }; export class FormatExpression implements Expression { @@ -69,10 +70,21 @@ export class FormatExpression implements Expression { if (!textColor) return null; } + let verticalAlign = null; + if (arg['vertical-align']) { + if (typeof arg['vertical-align'] === 'string' && !VERTICAL_ALIGN_OPTIONS.includes(arg['vertical-align'] as VerticalAlign)) { + return context.error(`'vertical-align' must be one of: 'bottom', 'center', 'top' but found '${arg['vertical-align']}' instead.`) as null; + } + + verticalAlign = context.parse(arg['vertical-align'], 1, StringType); + if (!verticalAlign) return null; + } + const lastExpression = sections[sections.length - 1]; lastExpression.scale = scale; lastExpression.font = font; lastExpression.textColor = textColor; + lastExpression.verticalAlign = verticalAlign; } else { const content = context.parse(args[i], 1, ValueType); if (!content) return null; @@ -82,7 +94,7 @@ export class FormatExpression implements Expression { return context.error('Formatted text type must be \'string\', \'value\', \'image\' or \'null\'.') as null; nextTokenMayBeObject = true; - sections.push({content, scale: null, font: null, textColor: null}); + sections.push({content, scale: null, font: null, textColor: null, verticalAlign: null}); } } @@ -93,7 +105,14 @@ export class FormatExpression implements Expression { const evaluateSection = section => { const evaluatedContent = section.content.evaluate(ctx); if (typeOf(evaluatedContent) === ResolvedImageType) { - return new FormattedSection('', evaluatedContent, null, null, null); + return new FormattedSection( + '', + evaluatedContent, + null, + null, + null, + section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null + ); } return new FormattedSection( @@ -101,7 +120,8 @@ export class FormatExpression implements Expression { null, section.scale ? section.scale.evaluate(ctx) : null, section.font ? section.font.evaluate(ctx).join(',') : null, - section.textColor ? section.textColor.evaluate(ctx) : null + section.textColor ? section.textColor.evaluate(ctx) : null, + section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null ); }; @@ -120,6 +140,9 @@ export class FormatExpression implements Expression { if (section.textColor) { fn(section.textColor); } + if (section.verticalAlign) { + fn(section.verticalAlign); + } } } diff --git a/src/expression/types/formatted.ts b/src/expression/types/formatted.ts index 0c5c283c8..81780b004 100644 --- a/src/expression/types/formatted.ts +++ b/src/expression/types/formatted.ts @@ -1,19 +1,24 @@ import type {Color} from '../../expression/types/color'; import type {ResolvedImage} from '../types/resolved_image'; +export const VERTICAL_ALIGN_OPTIONS = ['bottom', 'center', 'top'] as const; +export type VerticalAlign = typeof VERTICAL_ALIGN_OPTIONS[number]; + export class FormattedSection { text: string; image: ResolvedImage | null; scale: number | null; fontStack: string | null; textColor: Color | null; + verticalAlign: VerticalAlign | null; - constructor(text: string, image: ResolvedImage | null, scale: number | null, fontStack: string | null, textColor: Color | null) { + constructor(text: string, image: ResolvedImage | null, scale: number | null, fontStack: string | null, textColor: Color | null, verticalAlign: VerticalAlign | null) { this.text = text; this.image = image; this.scale = scale; this.fontStack = fontStack; this.textColor = textColor; + this.verticalAlign = verticalAlign; } } @@ -25,7 +30,7 @@ export class Formatted { } static fromString(unformatted: string): Formatted { - return new Formatted([new FormattedSection(unformatted, null, null, null, null)]); + return new Formatted([new FormattedSection(unformatted, null, null, null, null, null)]); } isEmpty(): boolean { diff --git a/src/reference/v8.json b/src/reference/v8.json index c3f11a169..d4ad8ce8e 100644 --- a/src/reference/v8.json +++ b/src/reference/v8.json @@ -3226,13 +3226,13 @@ } }, "format": { - "doc": "Returns a `formatted` string for displaying mixed-format text in the `text-field` property. The input may contain a string literal or expression, including an [`'image'`](#image) expression. Strings may be followed by a style override object that supports the following properties:\n\n- `\"text-font\"`: Overrides the font stack specified by the root layout property.\n\n- `\"text-color\"`: Overrides the color specified by the root paint property.\n\n- `\"font-scale\"`: Applies a scaling factor on `text-size` as specified by the root layout property.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)\n\n - [Display and style rich text labels](https://maplibre.org/maplibre-gl-js/docs/examples/display-and-style-rich-text-labels/)", + "doc": "Returns a `formatted` string for displaying mixed-format text in the `text-field` property. The input may contain a string literal or expression, including an [`'image'`](#image) expression. Strings may be followed by a style override object that supports the following properties:\n\n- `\"text-font\"`: Overrides the font stack specified by the root layout property.\n\n- `\"text-color\"`: Overrides the color specified by the root paint property.\n\n- `\"font-scale\"`: Applies a scaling factor on `text-size` as specified by the root layout property.\n\n- `\"vertical-align\"`: Aligns vertically text section or image in relation to the row it belongs to. Possible values are: \n\t- `\"bottom\"` *default*: align the bottom of this section with the bottom of other sections.\n\"Visual\n\t- `\"center\"`: align the center of this section with the center of other sections.\n\"Visual\n\t- `\"top\"`: align the top of this section with the top of other sections.\n\"Visual\n\t- Refer to [the design proposal](https://github.com/maplibre/maplibre-style-spec/issues/832) for more details.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)\n\n - [Display and style rich text labels](https://maplibre.org/maplibre-gl-js/docs/examples/display-and-style-rich-text-labels/)", "example": { "syntax": { - "method": ["value", "{ \"text-font\": string, \"text-color\": color, \"font-scale\": number }", "..."], + "method": ["value", "{ \"text-font\": string, \"text-color\": color, \"font-scale\": number, \"vertical-align\": \"bottom\" | \"center\" | \"top\" }", "..."], "result": "formatted" }, - "value": ["format", ["upcase", ["get", "FacilityName"]], {"font-scale": 0.8}, "\n\n", {}, ["downcase", ["get", "Comments"]], {"font-scale": 0.6}] + "value": ["format", ["upcase", ["get", "FacilityName"]], {"font-scale": 0.8}, "\n\n", {}, ["downcase", ["get", "Comments"]], {"font-scale": 0.6, "vertical-align": "center"}] }, "group": "Types", "sdk-support": { @@ -3256,6 +3256,11 @@ "android": "7.3.0", "ios": "4.10.0" }, + "vertical-align": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/5043", + "android": "https://github.com/maplibre/maplibre-native/issues/3055", + "ios": "https://github.com/maplibre/maplibre-native/issues/3055" + }, "image": { "js": "1.6.0", "android": "8.6.0", diff --git a/test/integration/expression/tests/format/basic/test.json b/test/integration/expression/tests/format/basic/test.json index 203bcd228..4b133bace 100644 --- a/test/integration/expression/tests/format/basic/test.json +++ b/test/integration/expression/tests/format/basic/test.json @@ -20,6 +20,10 @@ "d", { "text-color": "rgb(0, 255, 0)" + }, + "e", + { + "vertical-align": "center" } ], "inputs": [ @@ -43,21 +47,24 @@ "image": null, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null }, { "text": "b", "image": null, "scale": 2, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null }, { "text": "c", "image": null, "scale": null, "fontStack": "a,b", - "textColor": null + "textColor": null, + "verticalAlign": null }, { "text": "d", @@ -69,7 +76,16 @@ "g": 1, "b": 0, "a": 1 - } + }, + "verticalAlign": null + }, + { + "text": "e", + "image": null, + "scale": null, + "fontStack": null, + "textColor": null, + "verticalAlign": "center" } ] } diff --git a/test/integration/expression/tests/format/coercion/test.json b/test/integration/expression/tests/format/coercion/test.json index 1e4851022..712638454 100644 --- a/test/integration/expression/tests/format/coercion/test.json +++ b/test/integration/expression/tests/format/coercion/test.json @@ -44,21 +44,24 @@ "image": null, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null }, { "text": "1", "image": null, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null }, { "text": "true", "image": null, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null } ] } diff --git a/test/integration/expression/tests/format/data-driven-font/test.json b/test/integration/expression/tests/format/data-driven-font/test.json index 25f380e1f..333441d21 100644 --- a/test/integration/expression/tests/format/data-driven-font/test.json +++ b/test/integration/expression/tests/format/data-driven-font/test.json @@ -42,7 +42,8 @@ "image": null, "scale": 1.5, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null } ] }, @@ -53,7 +54,8 @@ "image": null, "scale": 0.5, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null } ] } diff --git a/test/integration/expression/tests/format/data-driven-vertical-align/test.json b/test/integration/expression/tests/format/data-driven-vertical-align/test.json new file mode 100644 index 000000000..9f3f8516e --- /dev/null +++ b/test/integration/expression/tests/format/data-driven-vertical-align/test.json @@ -0,0 +1,64 @@ +{ + "expression": [ + "format", + "a", + { + "vertical-align": [ + "get", + "vertical-align" + ] + } + ], + "inputs": [ + [ + {}, + { + "properties": { + "vertical-align": "center" + } + } + ], + [ + {}, + { + "properties": { + "vertical-align": "top" + } + } + ] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "formatted" + }, + "outputs": [ + { + "sections": [ + { + "text": "a", + "image": null, + "scale": null, + "fontStack": null, + "textColor": null, + "verticalAlign": "center" + } + ] + }, + { + "sections": [ + { + "text": "a", + "image": null, + "scale": null, + "fontStack": null, + "textColor": null, + "verticalAlign": "top" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/test/integration/expression/tests/format/image-sections/test.json b/test/integration/expression/tests/format/image-sections/test.json index 7bb844352..c769c5dd0 100644 --- a/test/integration/expression/tests/format/image-sections/test.json +++ b/test/integration/expression/tests/format/image-sections/test.json @@ -8,7 +8,10 @@ [ "image", "beach-11" - ] + ], + { + "vertical-align": "center" + } ], "inputs": [ [ @@ -38,7 +41,8 @@ }, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null }, { "text": "", @@ -48,7 +52,8 @@ }, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": "center" } ] } diff --git a/test/integration/expression/tests/format/implicit-coerce/test.json b/test/integration/expression/tests/format/implicit-coerce/test.json index d365f6ed8..4433dc0d6 100644 --- a/test/integration/expression/tests/format/implicit-coerce/test.json +++ b/test/integration/expression/tests/format/implicit-coerce/test.json @@ -56,7 +56,8 @@ "image": null, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null } ] }, @@ -67,7 +68,8 @@ "image": null, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null } ] }, @@ -78,7 +80,8 @@ "image": null, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null } ] } diff --git a/test/integration/expression/tests/format/implicit-omit/test.json b/test/integration/expression/tests/format/implicit-omit/test.json index 1d253a3e6..6c094c603 100644 --- a/test/integration/expression/tests/format/implicit-omit/test.json +++ b/test/integration/expression/tests/format/implicit-omit/test.json @@ -60,7 +60,8 @@ "image": null, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null } ] }, @@ -71,7 +72,8 @@ "image": null, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null } ] }, @@ -82,7 +84,8 @@ "image": null, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null } ] } diff --git a/test/integration/expression/tests/format/implicit/test.json b/test/integration/expression/tests/format/implicit/test.json index 93fe5924b..97d824d2c 100644 --- a/test/integration/expression/tests/format/implicit/test.json +++ b/test/integration/expression/tests/format/implicit/test.json @@ -53,7 +53,8 @@ "image": null, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null } ] }, @@ -64,7 +65,8 @@ "image": null, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null } ] }, @@ -75,7 +77,8 @@ "image": null, "scale": null, "fontStack": null, - "textColor": null + "textColor": null, + "verticalAlign": null } ] } diff --git a/test/integration/expression/tests/format/unknown-vertical-align/test.json b/test/integration/expression/tests/format/unknown-vertical-align/test.json new file mode 100644 index 000000000..d2d1cc0d7 --- /dev/null +++ b/test/integration/expression/tests/format/unknown-vertical-align/test.json @@ -0,0 +1,20 @@ +{ + "expression": [ + "format", + "a", + { + "vertical-align": "unknown" + } + ], + "expected": { + "compiled": { + "result": "error", + "errors": [ + { + "key": "", + "error": "'vertical-align' must be one of: 'bottom', 'center', 'top' but found 'unknown' instead." + } + ] + } + } +} \ No newline at end of file