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
\n\t- `\"center\"`: align the center of this section with the center of other sections.\n
\n\t- `\"top\"`: align the top of this section with the top of other sections.\n
\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