From 9a2c511755f16ed6960daf37becb53a83b635ec8 Mon Sep 17 00:00:00 2001 From: Chris Loer Date: Fri, 27 Jul 2018 12:28:50 -0700 Subject: [PATCH] "concat"/"formatted" -> "format" --- build/generate-flow-typed-style-spec.js | 2 + build/generate-style-code.js | 6 +- debug/streets-v10-viewport.json | 32 ++-- docs/components/expression-metadata.js | 7 +- docs/pages/style-spec.js | 16 ++ flow-typed/style-spec.js | 4 +- src/data/bucket/symbol_bucket.js | 2 +- .../expression/definitions/formatted.js | 137 ++++++++++-------- .../expression/definitions/index.js | 27 +--- .../expression/definitions/literal.js | 7 +- src/style-spec/expression/types.js | 1 + src/style-spec/reference/v8.json | 4 +- src/style-spec/validate/validate_formatted.js | 2 - .../background_style_layer_properties.js | 2 + .../circle_style_layer_properties.js | 2 + .../fill_extrusion_style_layer_properties.js | 2 + .../fill_style_layer_properties.js | 2 + .../heatmap_style_layer_properties.js | 2 + .../hillshade_style_layer_properties.js | 2 + src/style/style_layer/layer_properties.js.ejs | 2 + .../line_style_layer_properties.js | 2 + .../raster_style_layer_properties.js | 2 + .../symbol_style_layer_properties.js | 4 +- .../formatted => formatted/basic}/test.json | 84 +++++------ .../formatted/to-string/test.json | 42 ++++++ .../text-field/formatted-line/expected.png | Bin 612 -> 30757 bytes .../text-field/formatted-line/style.json | 8 +- .../text-field/formatted/style.json | 10 +- test/unit/style-spec/spec.test.js | 3 +- 29 files changed, 248 insertions(+), 168 deletions(-) rename test/integration/expression-tests/{concat/formatted => formatted/basic}/test.json (59%) create mode 100644 test/integration/expression-tests/formatted/to-string/test.json diff --git a/build/generate-flow-typed-style-spec.js b/build/generate-flow-typed-style-spec.js index d7a4070a801..0c6749682d1 100644 --- a/build/generate-flow-typed-style-spec.js +++ b/build/generate-flow-typed-style-spec.js @@ -114,6 +114,8 @@ fs.writeFileSync('flow-typed/style-spec.js', `// Generated code; do not edit. Ed declare type ColorSpecification = string; +declare type FormattedSpecification = string; + declare type FilterSpecification = | ['has', string] | ['!has', string] diff --git a/build/generate-style-code.js b/build/generate-style-code.js index 8fa49c1bb21..70e7b852b2b 100644 --- a/build/generate-style-code.js +++ b/build/generate-style-code.js @@ -5,6 +5,7 @@ const fs = require('fs'); const ejs = require('ejs'); const spec = require('../src/style-spec/reference/v8'); const Color = require('../src/style-spec/util/color'); +const {Formatted} = require('../src/style-spec/expression/definitions/formatted'); global.camelize = function (str) { return str.replace(/(?:^|-)(.)/g, function (_, x) { @@ -24,6 +25,8 @@ global.flowType = function (property) { return Object.keys(property.values).map(JSON.stringify).join(' | '); case 'color': return `Color`; + case 'formatted': + return `string | Formatted`; case 'array': if (property.length) { return `[${new Array(property.length).fill(flowType({type: property.value})).join(', ')}]`; @@ -61,6 +64,8 @@ global.runtimeType = function (property) { return 'StringType'; case 'color': return `ColorType`; + case 'formatted': + return `FormattedType`; case 'array': if (property.length) { return `array(${runtimeType({type: property.value})}, ${property.length})`; @@ -131,4 +136,3 @@ const layers = Object.keys(spec.layer.type.values).map((type) => { for (const layer of layers) { fs.writeFileSync(`src/style/style_layer/${layer.type.replace('-', '_')}_style_layer_properties.js`, propertiesJs(layer)) } - diff --git a/debug/streets-v10-viewport.json b/debug/streets-v10-viewport.json index e9abc573e4a..6567e4afc9a 100644 --- a/debug/streets-v10-viewport.json +++ b/debug/streets-v10-viewport.json @@ -2853,10 +2853,10 @@ "text-padding": 1, "text-rotation-alignment": "map", "text-pitch-alignment": "viewport", - "text-field": ["concat", - ["formatted", ["get", "name"], { "font-scale": 1.2 }], - ["formatted", " - ", {}], - ["formatted", ["get", "name_en"], { "font-scale": 0.8, "text-font": ["literal", [ "DIN Offc Pro Italic", "Arial Unicode MS Regular" ]] }] + "text-field": ["format", + ["get", "name"], { "font-scale": 1.2 }, + " - ", {}, + ["get", "name_en"], { "font-scale": 0.8, "text-font": ["literal", [ "DIN Offc Pro Italic", "Arial Unicode MS Regular" ]] } ], "text-letter-spacing": 0.01 }, @@ -2888,10 +2888,10 @@ "text-padding": 1, "text-rotation-alignment": "map", "text-pitch-alignment": "viewport", - "text-field": ["concat", - ["formatted", ["get", "name"], { "font-scale": 1.2 }], - ["formatted", " - ", {}], - ["formatted", ["get", "name_en"], { "font-scale": 0.8, "text-font": ["literal", [ "DIN Offc Pro Italic", "Arial Unicode MS Regular" ]] }] + "text-field": ["format", + ["get", "name"], { "font-scale": 1.2 }, + " - ", {}, + ["get", "name_en"], { "font-scale": 0.8, "text-font": ["literal", [ "DIN Offc Pro Italic", "Arial Unicode MS Regular" ]] } ], "text-letter-spacing": 0.01 }, @@ -2917,10 +2917,10 @@ "text-padding": 1, "text-rotation-alignment": "map", "text-pitch-alignment": "viewport", - "text-field": ["concat", - ["formatted", ["get", "name"], { "font-scale": 1.2 }], - ["formatted", " - ", {}], - ["formatted", ["get", "name_en"], { "font-scale": 0.8, "text-font": ["literal", [ "DIN Offc Pro Italic", "Arial Unicode MS Regular" ]] }] + "text-field": ["format", + ["get", "name"], { "font-scale": 1.2 }, + " - ", {}, + ["get", "name_en"], { "font-scale": 0.8, "text-font": ["literal", [ "DIN Offc Pro Italic", "Arial Unicode MS Regular" ]] } ], "text-letter-spacing": 0.01 }, @@ -3699,10 +3699,10 @@ }, "text-offset": { "base": 1, "stops": [ [ 7.99, [ 0, 0.15 ] ], [ 8, [ 0, 0 ] ] ] }, "text-anchor": { "base": 1, "stops": [ [ 7, "top" ], [ 8, "center" ] ] }, - "text-field": ["concat", - ["formatted", ["get", "name_en"], { "font-scale": 1.2 }], - ["formatted", "\n", {}], - ["formatted", ["get", "name"], { "font-scale": 0.8, "text-font": ["literal", [ "DIN Offc Pro Italic", "Arial Unicode MS Regular" ]] }] + "text-field": ["format", + ["get", "name"], { "font-scale": 1.2 }, + "\n", {}, + ["get", "name_en"], { "font-scale": 0.8, "text-font": ["literal", [ "DIN Offc Pro Italic", "Arial Unicode MS Regular" ]] } ], "text-max-width": 7, "text-size": { "base": 0.9, "stops": [ [ 4, 12 ], [ 10, 32 ] ] } diff --git a/docs/components/expression-metadata.js b/docs/components/expression-metadata.js index 2c217674b0f..15c8f55741d 100644 --- a/docs/components/expression-metadata.js +++ b/docs/components/expression-metadata.js @@ -135,11 +135,12 @@ const types = { type: 'collator', parameters: [ '{ "case-sensitive": boolean, "diacritic-sensitive": boolean, "locale": string }' ] }], - formatted: [{ + format: [{ type: 'formatted', parameters: [ - 'string', - '{ "font-scale": number, "text-font": array }' + 'input_1: string, options_1: { "font-scale": number, "text-font": array }', + '...', + 'input_n: string, options_n: { "font-scale": number, "text-font": array }' ] }] }; diff --git a/docs/pages/style-spec.js b/docs/pages/style-spec.js index a55e6a056dc..efc2532c968 100644 --- a/docs/pages/style-spec.js +++ b/docs/pages/style-spec.js @@ -159,6 +159,9 @@ const navigation = [ { "title": "String" }, + { + "title": "Formatted" + }, { "title": "Boolean" }, @@ -948,6 +951,19 @@ export default class extends React.Component {

Especially of note is the support for hsl, which can be easier to reason about than rgb().

+
+ +

Formatted

+

The formatted type represents a string broken into sections annotated with separate formatting options.

+ {highlightJSON(` + { + "text-field": ["format", + "foo", { "font-scale": 1.2 }, + "bar", { "font-scale": 0.8 } + ] + }`)} +
+

String

diff --git a/flow-typed/style-spec.js b/flow-typed/style-spec.js index 5f96263d5fe..64d51866e31 100644 --- a/flow-typed/style-spec.js +++ b/flow-typed/style-spec.js @@ -2,6 +2,8 @@ declare type ColorSpecification = string; +declare type FormattedSpecification = string; + declare type FilterSpecification = | ['has', string] | ['!has', string] @@ -223,7 +225,7 @@ declare type SymbolLayerSpecification = {| "icon-pitch-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">, "text-pitch-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">, "text-rotation-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">, - "text-field"?: DataDrivenPropertyValueSpecification, + "text-field"?: DataDrivenPropertyValueSpecification, "text-font"?: DataDrivenPropertyValueSpecification>, "text-size"?: DataDrivenPropertyValueSpecification, "text-max-width"?: DataDrivenPropertyValueSpecification, diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 68f524fe651..6f85864a84b 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -349,7 +349,7 @@ class SymbolBucket implements Bucket { const textField = layout.get('text-field'); const iconImage = layout.get('icon-image'); const hasText = - (textField.value.kind !== 'constant' || textField.value.value.length > 0) && + (textField.value.kind !== 'constant' || textField.value.value.toString().length > 0) && (textFont.value.kind !== 'constant' || textFont.value.value.length > 0); const hasIcon = iconImage.value.kind !== 'constant' || iconImage.value.value && iconImage.value.value.length > 0; diff --git a/src/style-spec/expression/definitions/formatted.js b/src/style-spec/expression/definitions/formatted.js index fc24419ba48..b9f5238e51e 100644 --- a/src/style-spec/expression/definitions/formatted.js +++ b/src/style-spec/expression/definitions/formatted.js @@ -18,13 +18,6 @@ export class FormattedSection { this.scale = scale; this.fontStack = fontStack; } - - serialize() { - const fontStack = this.fontStack ? - ["literal", this.fontStack.split(',')] : - null; - return ["formatted", this.text, { "text-font": fontStack, "font-scale": this.scale }]; - } } export class Formatted { @@ -35,77 +28,96 @@ export class Formatted { } toString(): string { - return this.sections.map(section => section.text).join(); + return this.sections.map(section => section.text).join(''); } serialize() { - if (this.sections.length === 1) { - return this.sections[0].serialize(); - } else { - return ["concat"].concat(this.sections.map(section => section.serialize())); + const serialized = ["format"]; + for (const section of this.sections) { + serialized.push(section.text); + const fontStack = section.fontStack ? + ["literal", section.fontStack.split(',')] : + null; + serialized.push({ "text-font": fontStack, "font-scale": section.scale }); } + return serialized; } } -export class FormattedExpression implements Expression { - type: Type; - text: Expression; +type FormattedSectionExpression = { + text: Expression, scale: Expression | null; font: Expression | null; +} + +export class FormattedExpression implements Expression { + type: Type; + sections: Array; - constructor(text: Expression, scale: Expression | null, font: Expression | null) { + constructor(sections: Array) { this.type = FormattedType; - this.text = text; - this.scale = scale; - this.font = font; + this.sections = sections; } static parse(args: Array, context: ParsingContext): ?Expression { - if (args.length !== 3) - return context.error(`Expected two arguments.`); - - const text = context.parse(args[1], 1, ValueType); - if (!text) return null; - const kind = text.type.kind; - if (kind !== 'string' && kind !== 'value' && kind !== 'null') - return context.error(`Formatted text type must be 'string', 'value', or 'null'.`); - - const options = (args[2]: any); - if (typeof options !== "object" || Array.isArray(options)) - return context.error(`Format options argument must be an object.`); - - let scale = null; - if (options['font-scale']) { - scale = context.parse(options['font-scale'], 1, NumberType); - if (!scale) return null; + if (args.length < 3) { + return context.error(`Expected at least two arguments.`); } - let font = null; - if (options['text-font']) { - font = context.parse(options['text-font'], 1, ValueType); // Require array of strings? - if (!font) return null; + if ((args.length - 1) % 2 !== 0) { + return context.error(`Expected an even number of arguments.`); } - return new FormattedExpression(text, scale, font); + const sections: Array = []; + for (let i = 1; i < args.length - 1; i += 2) { + const text = context.parse(args[i], 1, ValueType); + if (!text) return null; + const kind = text.type.kind; + if (kind !== 'string' && kind !== 'value' && kind !== 'null') + return context.error(`Formatted text type must be 'string', 'value', or 'null'.`); + + const options = (args[i + 1]: any); + if (typeof options !== "object" || Array.isArray(options)) + return context.error(`Format options argument must be an object.`); + + let scale = null; + if (options['font-scale']) { + scale = context.parse(options['font-scale'], 1, NumberType); + if (!scale) return null; + } + + let font = null; + if (options['text-font']) { + font = context.parse(options['text-font'], 1, ValueType); // Require array of strings? + if (!font) return null; + } + sections.push({text, scale, font}); + } + + return new FormattedExpression(sections); } evaluate(ctx: EvaluationContext) { - return new Formatted([ - new FormattedSection( - this.text.evaluate(ctx) || "", - this.scale ? this.scale.evaluate(ctx) : null, - this.font ? this.font.evaluate(ctx).join(',') : null + return new Formatted( + this.sections.map(section => + new FormattedSection( + section.text.evaluate(ctx) || "", + section.scale ? section.scale.evaluate(ctx) : null, + section.font ? section.font.evaluate(ctx).join(',') : null + ) ) - ]); + ); } eachChild(fn: (Expression) => void) { - fn(this.text); - if (this.scale) { - fn(this.scale); - } - if (this.font) { - fn(this.font); + for (const section of this.sections) { + fn(section.text); + if (section.scale) { + fn(section.scale); + } + if (section.font) { + fn(section.font); + } } } @@ -116,13 +128,18 @@ export class FormattedExpression implements Expression { } serialize() { - const options = {}; - if (this.scale) { - options['font-scale'] = this.scale.serialize(); - } - if (this.font) { - options['text-font'] = this.font.serialize(); + const serialized = ["format"]; + for (const section of this.sections) { + serialized.push(section.text.serialize()); + const options = {}; + if (section.scale) { + options['font-scale'] = section.scale.serialize(); + } + if (section.font) { + options['text-font'] = section.font.serialize(); + } + serialized.push(options); } - return ["formatted", this.text.serialize(), options]; + return serialized; } } diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index e91e5997dbf..c36001e97db 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -1,6 +1,6 @@ // @flow -import { NumberType, StringType, BooleanType, ColorType, ObjectType, ValueType, ErrorType, CollatorType, FormattedType, array, toString } from '../types'; +import { NumberType, StringType, BooleanType, ColorType, ObjectType, ValueType, ErrorType, CollatorType, array, toString } from '../types'; import { typeOf, Color, validateRGBA } from '../values'; import CompoundExpression from '../compound_expression'; @@ -47,7 +47,7 @@ const expressions: ExpressionRegistry = { 'case': Case, 'coalesce': Coalesce, 'collator': CollatorExpression, - 'formatted': FormattedExpression, + 'format': FormattedExpression, 'interpolate': Interpolate, 'length': Length, 'let': Let, @@ -541,24 +541,11 @@ CompoundExpression.register(expressions, { [StringType], (ctx, [s]) => s.evaluate(ctx).toLowerCase() ], - 'concat': { - type: StringType, - overloads: [ - [ - varargs(StringType), - (ctx, args) => args.map(arg => arg.evaluate(ctx)).join('') - ], - [ - varargs(FormattedType), - (ctx, args) => { - return new Formatted( - args.map(arg => arg.evaluate(ctx).sections) - .reduce((acc, val) => acc.concat(val), []) - ); - } - ] - ] - }, + 'concat': [ + StringType, + varargs(StringType), + (ctx, args) => args.map(arg => arg.evaluate(ctx)).join('') + ], 'resolved-locale': [ StringType, [CollatorType], diff --git a/src/style-spec/expression/definitions/literal.js b/src/style-spec/expression/definitions/literal.js index 09146fec3d5..d686aa6e036 100644 --- a/src/style-spec/expression/definitions/literal.js +++ b/src/style-spec/expression/definitions/literal.js @@ -62,16 +62,13 @@ class Literal implements Expression { // so we have to implement an equivalent serialization here return ["rgba"].concat(this.value.toArray()); } else if (this.value instanceof Formatted) { - // Constant-folding can generate Literal expressions that you - // couldn't actually generate with a "literal" expression, - // so we have to implement an equivalent serialization here + // Same as Color return this.value.serialize(); } else { assert(this.value === null || typeof this.value === 'string' || typeof this.value === 'number' || - typeof this.value === 'boolean' || - typeof this.value === 'formatted'); + typeof this.value === 'boolean'); return (this.value: any); } } diff --git a/src/style-spec/expression/types.js b/src/style-spec/expression/types.js index 02f2aaff719..5a6cd4c2d8d 100644 --- a/src/style-spec/expression/types.js +++ b/src/style-spec/expression/types.js @@ -66,6 +66,7 @@ const valueMemberTypes = [ StringType, BooleanType, ColorType, + FormattedType, ObjectType, array(ValueType) ]; diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index 9698f1c624d..93750fe70d4 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -1508,7 +1508,7 @@ "type": "formatted", "default": "", "tokens": true, - "doc": "Value to use for a text label.", + "doc": "Value to use for a text label. If a plain `string` is provided, it will be treated as a `formatted` with default/inherited formatting options.", "sdk-support": { "basic functionality": { "js": "0.10.0", @@ -2505,7 +2505,7 @@ } } }, - "formatted": { + "format": { "doc": "Returns `formatted` text containing annotations for use in mixed-format `text-field` entries. If set, the `text-font` argument overrides the font specified by the root layout properties. If set, the `font-scale` argument specifies a scaling factor relative to the `text-size` specified in the root layout properties.", "group": "Types", "sdk-support": { diff --git a/src/style-spec/validate/validate_formatted.js b/src/style-spec/validate/validate_formatted.js index 4cac73e2450..61ed402df3d 100644 --- a/src/style-spec/validate/validate_formatted.js +++ b/src/style-spec/validate/validate_formatted.js @@ -1,6 +1,4 @@ // @flow - -import ValidationError from '../error/validation_error'; import validateExpression from './validate_expression'; import validateString from './validate_string'; diff --git a/src/style/style_layer/background_style_layer_properties.js b/src/style/style_layer/background_style_layer_properties.js index 5b1cbacbefd..182cd821891 100644 --- a/src/style/style_layer/background_style_layer_properties.js +++ b/src/style/style_layer/background_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type PaintProps = {| "background-color": DataConstantProperty, diff --git a/src/style/style_layer/circle_style_layer_properties.js b/src/style/style_layer/circle_style_layer_properties.js index 3e9176f6947..9321d797397 100644 --- a/src/style/style_layer/circle_style_layer_properties.js +++ b/src/style/style_layer/circle_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type PaintProps = {| "circle-radius": DataDrivenProperty, diff --git a/src/style/style_layer/fill_extrusion_style_layer_properties.js b/src/style/style_layer/fill_extrusion_style_layer_properties.js index ac0966a83be..78a29cdcf18 100644 --- a/src/style/style_layer/fill_extrusion_style_layer_properties.js +++ b/src/style/style_layer/fill_extrusion_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type PaintProps = {| "fill-extrusion-opacity": DataConstantProperty, diff --git a/src/style/style_layer/fill_style_layer_properties.js b/src/style/style_layer/fill_style_layer_properties.js index 9851f734508..a62dd756e1c 100644 --- a/src/style/style_layer/fill_style_layer_properties.js +++ b/src/style/style_layer/fill_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type PaintProps = {| "fill-antialias": DataConstantProperty, diff --git a/src/style/style_layer/heatmap_style_layer_properties.js b/src/style/style_layer/heatmap_style_layer_properties.js index dd53a9ee9a0..d425156c001 100644 --- a/src/style/style_layer/heatmap_style_layer_properties.js +++ b/src/style/style_layer/heatmap_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type PaintProps = {| "heatmap-radius": DataDrivenProperty, diff --git a/src/style/style_layer/hillshade_style_layer_properties.js b/src/style/style_layer/hillshade_style_layer_properties.js index f16f9b51fd6..3f2e5c2ecdd 100644 --- a/src/style/style_layer/hillshade_style_layer_properties.js +++ b/src/style/style_layer/hillshade_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type PaintProps = {| "hillshade-illumination-direction": DataConstantProperty, diff --git a/src/style/style_layer/layer_properties.js.ejs b/src/style/style_layer/layer_properties.js.ejs index d9740285990..7bf3c2bf567 100644 --- a/src/style/style_layer/layer_properties.js.ejs +++ b/src/style/style_layer/layer_properties.js.ejs @@ -19,6 +19,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + <% if (layoutProperties.length) { -%> export type LayoutProps = {| <% for (const property of layoutProperties) { -%> diff --git a/src/style/style_layer/line_style_layer_properties.js b/src/style/style_layer/line_style_layer_properties.js index bef2f2ed44a..442d67d04b3 100644 --- a/src/style/style_layer/line_style_layer_properties.js +++ b/src/style/style_layer/line_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type LayoutProps = {| "line-cap": DataConstantProperty<"butt" | "round" | "square">, "line-join": DataDrivenProperty<"bevel" | "round" | "miter">, diff --git a/src/style/style_layer/raster_style_layer_properties.js b/src/style/style_layer/raster_style_layer_properties.js index e0193d510b2..4c0e28bb8f3 100644 --- a/src/style/style_layer/raster_style_layer_properties.js +++ b/src/style/style_layer/raster_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type PaintProps = {| "raster-opacity": DataConstantProperty, diff --git a/src/style/style_layer/symbol_style_layer_properties.js b/src/style/style_layer/symbol_style_layer_properties.js index ed22587f068..1d06c35d6bd 100644 --- a/src/style/style_layer/symbol_style_layer_properties.js +++ b/src/style/style_layer/symbol_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type LayoutProps = {| "symbol-placement": DataConstantProperty<"point" | "line" | "line-center">, "symbol-spacing": DataConstantProperty, @@ -34,7 +36,7 @@ export type LayoutProps = {| "icon-pitch-alignment": DataConstantProperty<"map" | "viewport" | "auto">, "text-pitch-alignment": DataConstantProperty<"map" | "viewport" | "auto">, "text-rotation-alignment": DataConstantProperty<"map" | "viewport" | "auto">, - "text-field": DataDrivenProperty, + "text-field": DataDrivenProperty, "text-font": DataDrivenProperty>, "text-size": DataDrivenProperty, "text-max-width": DataDrivenProperty, diff --git a/test/integration/expression-tests/concat/formatted/test.json b/test/integration/expression-tests/formatted/basic/test.json similarity index 59% rename from test/integration/expression-tests/concat/formatted/test.json rename to test/integration/expression-tests/formatted/basic/test.json index dffabf25f70..eef30eb57f4 100644 --- a/test/integration/expression-tests/concat/formatted/test.json +++ b/test/integration/expression-tests/formatted/basic/test.json @@ -1,27 +1,22 @@ { "expression": [ - "concat", + "format", "a", - [ - "formatted", - "b", - { - "font-scale": 2 - } - ], - [ - "formatted", - "c", - { - "text-font": [ - "literal", - [ - "a", - "b" - ] + {}, + "b", + { + "font-scale": 2 + }, + "c", + { + "text-font": [ + "literal", + [ + "a", + "b" ] - } - ] + ] + } ], "inputs": [ [ @@ -34,7 +29,7 @@ "result": "success", "isFeatureConstant": true, "isZoomConstant": true, - "type": "string" + "type": "formatted" }, "outputs": [ { @@ -58,31 +53,28 @@ } ], "serialized": [ - "concat", - [ - "formatted", - "a", - { - "font-scale": null, - "text-font": null - } - ], - [ - "formatted", - "b", - { - "font-scale": 2, - "text-font": null - } - ], - [ - "formatted", - "c", - { - "font-scale": null, - "text-font": ["literal", ["a", "b"]] - } - ] + "format", + "a", + { + "font-scale": null, + "text-font": null + }, + "b", + { + "font-scale": 2, + "text-font": null + }, + "c", + { + "font-scale": null, + "text-font": [ + "literal", + [ + "a", + "b" + ] + ] + } ] } } diff --git a/test/integration/expression-tests/formatted/to-string/test.json b/test/integration/expression-tests/formatted/to-string/test.json new file mode 100644 index 00000000000..2c505712fbd --- /dev/null +++ b/test/integration/expression-tests/formatted/to-string/test.json @@ -0,0 +1,42 @@ +{ + "expression": [ + "to-string", + [ + "format", + "a", + {}, + "b", + { + "font-scale": 2 + }, + "c", + { + "text-font": [ + "literal", + [ + "a", + "b" + ] + ] + } + ] + ], + "inputs": [ + [ + {}, + {} + ] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": true, + "isZoomConstant": true, + "type": "string" + }, + "outputs": [ + "abc" + ], + "serialized": "abc" + } +} diff --git a/test/integration/render-tests/text-field/formatted-line/expected.png b/test/integration/render-tests/text-field/formatted-line/expected.png index 2e8583d07199dad8c769d409e9668b8886a8952b..a5c6a684a0854f0216264415251fbbedd0ed865a 100644 GIT binary patch literal 30757 zcmYhEcU;eH`~O{+mA$h?B`dUKMP^Ad;tHX?kd;+N8IdS0qKqU-S7?}7DN3nSGBPVA zo03r^-{+C*{yl#8Klgoy&*%L<&*MCf*Xwn>jx+D+Qw>`*?b5Vfy?QN-CQUG{SMP8B z=U?>{8u4GL{&yVf)m!?(Xu{Z8u7ABxZZf9Tx00&guYSIEO}hN)-Ls98ye9XXc*k?{ zQX6gC(Pd9Z`}$hj9a!J#KuFA^r|p%$Z!~_{x2?g5zDMsyUQW99yYO04&iBfn8}!4v zJ7-(?c^wN651*kN&{JVcxp9Z~?KNCVCp~@objQw}FS3Ug+TA%g{L7nsuSny42`fqr z+^dQko1a^hnz<)1FomDb=-b|>$EYqthYp>3XhiCTLibEVrH!8R2Sxq+@6P8}*GHT_ zeJ5;zt*wfml73oNmQP-U;hA+`7kNIkT(F?KUyo5X8G*eE9zD`r@nXVplLKjQ@}s? z@}&CV(HnPe-`?o$+qX4l!NI|H5ku;;@Av-(e*J$v7sLnBMxz1uZ^P@e$)tJs^^xT3i`9>&h2cxZ&2!k2f-{{1b^SgM)~pM$Ft1L z64rjQP%{bjyn1z1<*OXWXO~uY8}8z%cKY<`Ns*@R&9t?(H*DOP=<>4Z0o^4plD9Ul z`LN96Mt*+K!;tXsUKSP>3Lf3HLYgPepW3c#*RD&SUoqm7hYuglKmPIkQx2P5aB+o- z(#9`sJbpeYEdTno_po7|1`Zq;dhfvlEyp|EbeBE%2@UPianSrR4=$~KxBuu-W&Y!z z`BGs^&zf%h*or42FhU=knx_(!G0g3=s=d9vorA-R_wTbte!8-C{-9f|a8Z%fqD70U zK4%V&kB?8je}6BZ)4plLq~L&nHtz23HB}{k^AlaR9XZldZ`7!O3_FFCcax%Lwe8et z$MNH83l}cjvv+TCadC1-%MF{iZEKj9m*=!_Vcvv6`znL_OgpUMQM-YM+OO|1`jq*( zaSjd+irTAQPv5X%L+b`(s}i%avUsn^B~Q-xZQs;RtFw`j(bL40UI!0$Gw|zSVr0}L zGk9Q0S@r?`lrMjjeM-N=4d!%bt-ikP8igf|M>Ca?2;$JQPaW_7Txd9hn$*s z!C=rn6@^%pZR5tqn4MB+)=XvCu>BE+ehC|Yt|6MIJvqOmdiJ8li-&Kh(zt*BzNe>W z(c8B>-@w@zaUuXlq;Bt>eOr`hw}S$B!Qm>U$j4cD&=iZ{MvQZB!;18>@%HWH@#E`XSXO9YY}{RbG~ZdXXSZwJdei%Nu}Qo84Ib=RS(d$_GJEL3(Hle6cJJOjNwuw9h>&-S$Cp({`_v2E?qk7{HX2N)}CR; zmX=|?D+A(fPm!MUq6St*9`Oc{j;!Nzkb&59-L~fH2UD050CQ- z3etakeV6F|Yea;B@6m*WKyrc2k|n;UPY>d8dFR@8JnWY*Us^XP{(ZqdQ;|cwjYM($ z`0-AiI|uCFzhhkZn6giwlqy+@h}hViIO`joG_2~ehsPo#+w|zMdy3k4{_Q@~zU><{ zYGmW=>@$Cmp`oD{IZ18m!SpoWuKm>2?LL=gbn4W}fA8MZqZ4-#t@c&r#Wzz^5PFDOFhi?S1&MW1c=f&37y5Pfc?F<*&b?s&(|N z$rqMBt9ly~t1)CqWI{q)&a-S?GuHFRw-3faEZ>V4FSd|cU%q_lGo}tqpE*;O!@m66 zx3niu4yetTF(d2H$bGFwSJkUuzy8$2dPdWyx8A&Yb2mL#|E@!q#w{-%cWLdXsjF8H zY22zuDf_6m?#qsa`NxYteabnzFrSaoU%7g<{o}I>+jrg0gZ|&AeT=}Zy!_x~?)YY3unr3jWs|??~dBY|hF^%S1ErTYh5&(HOZ{OZY z!Roo}$@zg{8$LH#{NQA9%C=?&1qB)_%ciVawaUrmb#5=-E91!6t>qOJW98+SFJFE= zIe9Z7I;(#N@{4MpK0%?Ox7UBV>Xn!{+@z(_=qW)xMy;D}Y3WCTpzf%Vv!;Y-HKZn{ z+_2GAD?z+YqGMlnA&~x1j-mCkhf~mwCOl< z1Mj|XqFRvk;>Ej6qmL089A@g5rk0j+pFUnxtm2O!@1zH)xP5!y`M}U+$7LZxbeD9Y z;z}T#Tl~<}%*?>TqQ|yv+x8zg5OMtY_MQsA^Y2C)R~Kz<uV1k+GoUw z(1Zl7x+VDjae%y+FMoY6jN)HW?DD9c5|sm6wQ8lOr&pB`Z+olnfB`DX%72p? zD58;(k*u)h;s@&g{`>EoIIF+betQ10rn)lSPK%5-k*6S~+mm{n7B5y&NcsKG+_@Rx zihxS(H?Lop2i?0jivu<(=u^_~!&k28Jzw`u4uQpSz>4kCrBkWACX^)q47-!_69ezw zH7&~t^8*rCx>xreJ9e!4+=K>uC#Sx>A5;Be*WlNMN%~73pAA28Vu#xH?b{VnET8#w z)=tmPo_IS}!zs6?jg5_lj*eZrf3Nl(JFfildPa}Y8zz~Uw4k;R-bx)NLMD)!)Hnb~ zFI@_iZ@Y8Im`Hzh=TOP#mziHa6)JV?*m2L~-W@u0*dH27QZBf(dcd!r-yKN=SqFyt z{rvu^GKi8zxVH98>auw8;?kON6u+{{9DT>ynri1q3ER@r44!-Z-gxrVscPzJz~smi zF)`XKpR97smoKY!cN@Ot(4p>UYekretP<(2F0sq&zO-6YI*bQ z@=y~Vs#(WDcGaI>$~mnlxuw1KDV z?BaOmp!LshAI>`EZCO#0)~0pqF+oo*ESouf`j`n58hrcqO=tMJFMWxM2@@t9Id#f+ z@7@m4(a{$#UW{|Cu(@-1ROYu2Plmge_Z%@|1n6MK%$Zv+T^g~ae8et4zgI-)m$&zu zMbFx)Ue?h4FYgKCD9=rrG#MK{eK|?s;K74)uC5zG5d?c}9-LGY*jd{l_ww3hWmzHP z*`w%Fr#Lpf1UDBve%!W8m&rU){YK5@6o>5(3CW8z?g$K<(SNU=d(HmA*Q)+*)~p$+ z-ODukCV%COV~)%F0?l#b#=U-Us;g=A9+LyQa?+?ZPNoYM_)bV~6C^i++9hR$hbQaj-@Q97de&W4ef_YktSOh)e3%UM@oJ@{1`t@g zb}a?r#L1JYV3@rFW}9-9wlr+!^zd}|2M-=(9UgUn8bHeDDLQrOq8u=(wdcdlpx~Z8 z>+w(Bm3O&qE*t~0h>9}wYT4cCZb88$jwj)JD>HK!MZ?C?(eTsrt1Ff-FQSxf`2K7y zaLCHRf$WizY2)NHY0@MGfPk{Lc2M?#p+S50KrH%<8r7X_=Hug2#R=Tu<8#d8ckPK; z{fWIB9Lwm_r^~#Q+>3y6;BIRxt4)BL*0s*=H5G<~EUu(;HZ(Le#=t(nLbU-0_hmhP z94bJ>I;DYuPp4$Q2L{qxQ!{{ERPpihC(iJ=Agb~)(|&SncM&`$>%P7v(>1)j=7UY) zwT)n#wjDa8QqX41o*m_wa~ODVJ2TU0%9NYWpC8t=O`|kSnmW~hgq`&J*C!S}_xzH1 zBi4TO85e%Uh$o?*L|nSm1t3n|-;z7Oo4)(O{rfw)tXvt8`O+_4g>+ z_wLzU-}Kj&U*Dc^h)b?_-k^n(=I3@2p@i6uo;3la zm2qp<=q|Xt<|W&*XV0G8sA)<&ckQbF_GFn`dC_0`0hJ?PKTXsIaUYM54`6!!+>fzxboD5(<88{Xdm4C)@8i3Qu<>l=TqpOPQgR-C+$yr&=fn9)B&Ns_QLSlvL zJ5hz#ty|Y^*z!%Jyz-jrm89=2<%X=_1QQcqPtOMOaKCGRX=-R#K`{sitAz_Qrj^$0 zR&CoP%>9RinTM*at*wu*FN@{~L+-nGGT~>!88Kv&2AG|^o}KNxXHR?5u3L%kP$y^S z(xT+8f6h)CYn`~ETB~8hhC<9)K6q=!|14pevNUF-d59E2XJZrU>9ZyXgK- z(AoAIUahn_1FKfAuJ-N*AV}&OF>pniex+^$XkysJN8yBY`;DyuT z_WRd&BV%J*z_Y{@jPB{PXA4b9vvsTjDr7oiMw>GZ*)Q%z&!k3Ao;-Q~uocQTwzs%( z{rVTER?0ycuM6T;_+JKf$#WdUmg=>k`sF^sQ>YfaY2u2KM!ZSt z)29bdoakFqQxg{#R}_|EV=+dk;b7sUkak5yNwFvw#|`~@l$Mo!zR^lwW8D`g&Q|>S z^JSE=_s_2$IKSkHOVRbeg4O44IdGsW9PxcY{8sYfmMvTSC#jOi$i=rP<_jMs^j?>r`*1Md(yONeSlflHvXi(S2>z;ggt+IQ2=?}tT6pBfB)7lE-ngS zH;x5hY=)`nI1Y(hO_gTH!4`IZC|b#f4<_UyZyz5!{!7p8bAz$2kD!^+Cr%UrkDp%K zcmOKiu5H_K|NPT|=!Hpc&Isx=bJna0fau>l+42U(9+Ah78w62)w44hxIDn}vb0P;i zCrtkqo5fQ1&~v?haYe}vKR-u87-$R=Mn~YgC<7aRe6dd6((p>4L;4<0&{ zcmICm*|U4dEF~{rZi|oC{4KRs-qFKYIpE0jzSE;;!J!qY!KTrrr!G8C$<6f#7zr~p z*xk*Wm|OetY53}Q4_1A8-W|E8=M`s2uj{&XpTE4B30F%cOgJaEZr=11H1gr`5b)iO z$&oKC19Yx!5>YD#I-?0||MK;#8#QLd>eUnZX5i-G!#zyM)Oimc7=QrDXm8)WGnzcv z1`u#??|MI2OLuHrSU$RQG^Y7nJMXP`;#G}FhqtTgeqOGW*HI6cMy=ggZI5fihkXN{t-udnm7OMB;@aad87)fq_9N!Pg(Zy;%mGu)_Uw;epk@2-G# zh2JEU`tZ}&@7}5K;48t+xnz29@NyRy<-UCtfWYk_MAQkk{w4&uapT6J_Z~i+2`3On z>F!O^iCgu?^!#4yY4ut=&A~luT9H<5Z|~`+xA8}N zNjE+|68zK1_VEb`N9{9%NjtRxk&(TT2Zs$CmP^VH1W3;${K!K6_wM8X=&^oTM_bN=Z=Vxy2pHO|V!^(v9xY zr;~W)y2=Xg0R#0KzrETeX&iE4&NY3I*wNQF46)-!2nZt8p3(Cv$NB%O>0hu4fCiSE|OO3+PReF>gwvO{hC|hy-S~79LrOpDBQbu zZ@PuWRA8RISNr0me`e1==HpJ3nY&Ye5#W9zX_f&2Cu!nZ(r zI!&52D>=0GdH9hd^(-wdMSjZZZCEv!R7M8uN>kLgJM2+#Ol=;1f3|4Wh;pboy2 zzO%ysvi^TDV555#Z zppfNc#mc|`{!faPrRO~PL+yRL4t1q~=RSDgz`2w&VSl?lL~U-8`zXkD>C2baC_@k= z$eEgoO8sTamPrivL$PtE#NGf(=RJJbheuTd7^w^#=nEG#zp&Kz!iAyhzklov5HJ3^ zrsBm!4xXePR;k;_H9o)|l%;k64B_7U0#$1Ev2Z1yKKc3iwK8emym{ZjgQG~=uX4hI z{QbwE8X=rJP-yn-+oz4HDqtF7umh={3V~45k#t`8FXfwDvWH@64QM8G+Nn)Ba{Bb{ z_wSt*2b$lcIbh6Fwz076s_QJZ3@qgA?A&SC@}7qeABLo~>)7!Y7)MlwW{T=#)PgFE z)2o9PIyoU)d`?)ncyWH>N=J8i!4CQh=v&m3pu>mn6vW$#ddJ$IUHou2ahDfssW0b< z48bCvoOfZj8W>fSX+;AA1CeJY7#r^rDtBqM$dR2jZ5pJ!izSRGCHat)Sc|Kui_oIB z2%ktVy?XT`7m_uS_E5WORi)~1=pLP2*slNHrYl#jwCF${CQI|LUR?fk?M7;1x)jSx2Ixya4Eg-b0raE5e~3NSA?B2lasi z*`kTAR}(K?68H&rl;bYh(A?Y{()BsDqvdpS^V@`X^^npUZ%Jkwf2@K`3}0SUk7R8B z=5A#AOBV}W06ma|^J33=BguzHMco21Yaq$=?72yeZ1?_YVi4F0KAM}KUxHk9<>$xC z$d%U1mQ90+vH#*6O#jz)W+_I<+CiS;20}{x3>OZsbQz1BcgbGOD~n z>;=S~<5NPm%<6AnnjX-gL4#aW4HDzD!+POBzvkVBwN9LWC$<+H)xnvtgw|AWCM08n z#6)Hxkr68u>sPK_dkePy@4x+Pe*JocC_?$#@Fmai_3PJh>%T8WSoS=3ZU~V^l?@6C z8V`R)soYSW+?YKsDk|DUVXjm2(ZoOktEJ089j)Zsf(KhpS|?WHO9we5^&7nhI!J-JL z*4?{jv$?}oy>5*>*`RS_sUxCMn?~Cq;G=)PAUT;vqXv*qKEFHQn|%XXX9-_vv@;l)=^MgEfcr(oWn40~6S9nZu94XVR6? z37DiR@)b#_q_p(r!Qm?}x>c$H%|weSl!TgH0D^7OvgL8~HzdgwKff(en|9b|_wJV2 zH0zQPv(T#meVRHtrI3$sexHs#UJ)df$rl!W}EYCAkBFxd?A`%xt2{;;2U@hmzXT2LXQ-q+W(i+sz)>hHe z>xN<2>!Nv-jet3Z54*0wI@ZrA8Yd?Mg8{;T3}ONl*|#fwzp6H_n#QVnjqbbA?dtlDL;<|v(1_K_#K^v}vX%FU+qvhRJG0Qud+6<_ zG;k8PA3v^^eTc+}M!jIc0!f+i@!@Q>@wSaA)K{js zghqpCku4C&uIJ?VUA#D~|9}B*-BA`R5HjG@U3PaLX#g1KKjio|AD?l2@|X0@lUL++;tO-4YWjPQTTt6zoL+SyGA zVoO&1uH7iIHUX2Ko_?L~4&CLzurNP5gA~^N`}daz?cCXvH-narojJ1|ixXa*k)B>e zHKsQvNQD=<1=z|W$@imqxvcOSnojxs*_Orht7y9o4m#-Y8sTGtfq~89#hFB7`t94& z_C}WdJVKUF6xq^OuT=I*yUgSXljqRdE zw*g2Z7V$azOeD|&T`f>>ifAvolHw6NuU#7*J95B)0qW}N!_dE2XQ76ifYCo*t6;=% z3OsZuJ9=hR9IT9Bx_J5W8Tx-yrc7DBcCBOH(TQZn?dWWDT?}Y@_U_%Av)q%$F3(r% zDhY$aJ!QYfW(wlegTpqn?1Cog*P(L_vb;70frONz0tnb=GF97-KFt-kO3JVu9V?%1 z!`qG=InrV+e4}}1El*yZ>brl`dR^gO?5<9~zI}_(MM`;3;u0Ot3l#)JjIOo(&E4M8 zfFwp>9pOlv(X(zNm%K)`r212<6+HC#5h<{`*h}dI3LzAVAZrN?KEL$YZKQIJu?@1? zmk&?&%^o{d+rh>rB`VDQwxuswo_MfSY&k{-;KI>OZIZGu)$AkJKXR6%5}Up;Dwr;)5V<-@g43XvO5n zamoSZ{nHm3^w4+Dq642o4x&r;B6e!@tfRB~S9{YR2FU<)+5re*foLU-R+eS;8a&vq zU7xAaI^f{&EvXq9-blHZR=tsStCT*a7^+x7!JI2suOi~84jIw`30=0w#YKw~ZC!MI zbJdR@s9UakOLbIMtXP3BK`48zCBSGyZS|}7kI(G_msM6(*zy@~9-kXPm?80tWDNXg zb%iI098I9m4&^)^y>_j~v}x0T6m>AblI;J2NLuy2AOQW$r6jEd4c=QMuSbb1`&3j^ z2)*Rf#CL)$6T9=EgJ=RUot`y|VR(pwU`JQrJLNGA3`}yB{kOAI<#6ce=;X2iYd$?U zkt%~`88mHluReVm122H1ZZPs4J9g9waKE5_h;nnl4E!PWpcBnNn(a>1OKWRuc+!k{ z^LBw5VgteBq27R2IC4r-pP&p(3rCRz1U`@VY8im`Ja5_6RKPf`kDDk<7&L6CSDWiM z9=7p^4u8}B?lB`_EoeuryL8$4r|*>Vi3r>ZvYWnOK{ok6BQvvvHWD30TYLL$RCfNh zs)j~uFttVR;U4}R%M~wfHUV{4R94=2_^=~tR(WN{OBc!<1_EUrojo*Mp$!uq9Mk~r z(v&tcOCv$W$HiHp!`L}G+FDzW1v=8m;vCUa>RIDXAx1v4r;kUv6L3ujV-ou&l!>g1 zH>Bd%qRyrq=mG~g> z&YAfdY$|f5kOGDi{ZW{!7Kzuc`E{3U^8W%@C=IgmZpb;Z%G3-uzwh$RYKt z{QCAEku3pZDVh2I{xg8a0lBo4lEU$|ad0pQA`cT;H`#oE>CMNFy9gS1850|8*nclr zbSwEPIXM|ouad4f&0QZdgt(0?;05J7yN_741Hs}byc%iF%*+f)EWt6S2h9VjT21+l zR#&Ra4$uqjNjuJ>2cVU3l|Bc0fRy)kBsO~cd0PP8Aoo&IygLt6en#{X>C#E!0@T} z?(O;f*+tX`EjtBH!Z@_8;`@9@jW+ECcp?}>4ShkGt{!v@UP~l6zP!D6)$80kv31{o z{{2PSQdL!5POp>)gyabMr?-_;Ldhgm* zLa7y~N9x0busK(chFt>9BT-(_I|$tfv<`4K?GP!N?wt{|9I`{DQ^sK`$sC+y$6w;K z%^V<2kJlVwT1r%nw_ z8ah07I)DR=!O6$AC2K3Zh=hmkUJviz(W6Hp`V*acV0IHEC(7E9Bb$WqxYxLbs#{(= zNLHIUcdn7U`)Jg0a5Q=|SsZp;d?sf!w#}9lur==sHcyVU!M{Kk61(DN@VG~VezE>w z2&p<%#a;p|z(5XJ`;(6u1tA5zz{y7Jmb`l9m8%B`D1Q?bhP^54{i8D(Sy?w}COv&R zUj*%yD^*5~Is~H)6;p+r0ZU^OlSrx{Qh6~}EusVajnteR{oe=m0}swlr(d(JS;xTr z`_mywu5_NH|M=_2*Bdu(Y~}C)50z-nvx1S#_3}5l00j>^6 z>CRT8t2WUv`uzDb&b*he^CE~=l7eT_znq=3XU__Y$LJB2;n}3!MJ$~d6hyuNAJq0u z*x%80B0V(`M1a`60UDGUO}q5g2n2#Y=tas|3a^HY9rcF(e|>u&bA;q?`m|W4T7V_Q zTfxhN?I%!z(8Q5i#nv#}6jaYo=$fY?l+xTIF{fu`*-^5@04QID&yVt59g*&~P10KU zcrQqf4T=gDUa>?V^3I93X$bRH@EC0ztlmpSWhoYY(!wINwsAJv&4Yp?Qp@l**%tuti>y74L#=lg6>MVWI1D%*_D{ z+d@J*)9nZhY`gBu>&*Zk51rw$>BO3_JAlpni_8ClogSZhXfmg=tgLL++j~ulbEeIl z8AKk!GvSR~dF4vCZR0k}y9H2Z1kll@>(sTYHiGZB_m9Sdkxd?)oVyh=gLYEnr}nDD ziPNVIAf>P+YO2_Au^3^$<-s>kQA1*ESo}47R{w><>#*Zyp#VysVSo$1w?_%~fQS`U z<<`LMG!CW+lbE^1kWe<}&VV>NkK%>^=xb0I@gRM8av|u@A(KD$3`5sZZfdKz4dM^gmabDn}NGA)mFp?kE_W)b0 z?WE(>nk~WOhd-f@v@L0C6J#rB*Wo`JMzaRbVlxg4GEj{f z7#~+d80P`o-);^Tk4y@<3!p|^LR*xYmGaJxu+#IXJqSR_jWBEl`ZCCNNU6AR?wPEUYU|(z2eSR`N;|Qr3^O;f2Zth7wO_oxA0aNs$*AN&^Q8 z4aD!gW5;!D?Ywil_U%2{mgHnH=!Uin*|p19dTpXZ(Zbi6bm{6c4ERV^)HJGDb`Q-xFR#LPtAmPQ)VF7TqgqGAGS zCNKhoAt;>Jqy*jM*dyr2yHz@zo}W05P{0xfkmK)V`@WZoH22)14*ZNH?m2qnkKA*M zXM@bdHia~KP|vL`R*;h?PgXg?Yfmn?96V(4gYwsTD%)Fj-${Z;62ZV*{N{}}sE4xz zLJ{bz5G!U>1aR@U=_cr0xl*j_2ThFxUKh$nIT1J?@S>Db&#*HIo#|o7&K&Ppv9mq>4R`WQfk1 z%@)X(V^oBlZzHkE(FzamMgL6T6u^Kur(ODr)|P7T-T{XX3uhwV{fX+M=g$YzPlX^P z3+Lph8cm(*fM_SYg{ZzusLOr?+)Gd2fsP~|`)NYFXu;5WWv%Fj+#FiCVLYEo6Uqh@ zjv-O>Bf8%rE23y4B35Ya9Xf_;@X z$uMLSW*a6Pqx}K5IgfR6tf`T$4M4n4!b&Y-aKR4Lo}j#_Ug5{-^}C788+eBCGI^R zCNt7daCQAqGTseTWOUi0;^MJvap%sRdEmgaXLX?QG`w{jslQcy2;7T_i6mnvf&dse zjfmRdW8lUsn1MiYv9c(6)U?^SaU%%A6}rnAeP5Uq#;0+#wt&B-)tb1x_;0upD_6Jj z9*X1|u|k90R^YTG#IasFEm|b?3%Q4;(oN`(xWD+|P;H?`?O>naZ*1E1M$p5Id37XV z2TUW>!(cjOnt1Vrh=cLbK{xz(6UFvdAll8utHSdpvZdm~3ZL!|ASD%o5g5Cja%2hyZML!wCFWeecaP}pD7Muz zXSC}#ZYljDCHJb$uyFJzL&8^6Te~D@404vx_0d_$Q<}+NcvzqkPd?m9M}~Rp&|)6_vu`7b2(*9BFU5lIUGpk zzh}>c4IA{xr4WqIjy+$^7Bd`0(gBHq%q-Rv^j@@m@wfFh4$fuDfMh1T1xbkbkat8* z_QQ>frSi(}+MgFMU1Cr{Je9Nq(6VI(!V{Y|Z*B)@5d2LfeEy#F`w+hH1^CRezNt625Z>9Ci|L(Z@(6HLyUqh;!*Vpb=zxjntn~rX3dQ`2a*M(!H z8-t@0%}>9JKmKvu;?vP?cEKSFjxY4;xO1qRWzoj12L1aVRrX3BmGyOT?e8!@kHTM1 zmOe2nJo~*8WoK)n7Dr)#0ACjrinQI`cd2LAW!J&k&bsaJV35d9SSF2@;vYTBMxzr| zu@8ugo?)Bz?QiBA41c%2rZMD-6pWQRly^J>CJ^WfKWNcKXG}z%RRnIXzU>Rh0VPF6 z*8xTG7cRU4BfO+V4^jLJ!P!|%rO!8hUyyL-On`j|{;D{rILCryC}J+DlS5?yoz2L} zab&)SX^uu$E&MojJ!|fMSvLw*p*NHmpM%-SIb(rLzyr}u8U^5DIeVkr1fgEMa+R-tFDBq$|qEo2wT1czYr%&I+nb0=~RMped zi<^7JRg(k+0hib)`=xN)0G%TGx=UzSv8cY~WZyzgy6jHeQ(i_ zMdw0qkzp3ncV_qw&62v_9dF`(&Ix%=ZRE%U!&jCGEReLeYnLsy04JR;=ysXF6oGE; zzkaZ$ZNN=+Y2#s0Y3T&ycqYVTl#igvpH$Wdx#%bdos+*3DNvX+36Y{5H6y|R)!Tj0 zRf`-RiEh%76DJf9u&1cCgDKjxp!ZJA+lLg0S!g`?NMJK9M_Rkucp^mjgAyiUj%C2J zNUVlTjn(wsv3=}d;Z^kSxL)3ml}ybm2aOBb6>o@Xt?m#Y^?t~U=kvi zn%T`2+s!RVg8?aOm*POaL*~O^3*O_xua|weP*?`sKpojk){#daTjkNuaigD7KydJO z^HY-NUw>VPMR5l>gA!m6m0xk!crZAxx)3xmiN+aQ4dvTRKFB>eSGz|H85D5Q4X(fv zW9N-aq%UoLN)hh==Pl}1$Zwp4*$xt2+m0Q*kT#fgMdyfTju6RG>_~iWiN^ujjWo#d zPPJyfpw+^2=2-vTX!Q^Z1>!&vA=CZWIz+xbAa{o66}o+8xRvh;L*1sBr=0M2QH@;)TC}HBK+O95JFm<90+sV zTo@giYa~a0%ApbBchc$7V%h;dxHX~gt z&lm8i_>yeTnbSzHzPY1KUqd`ci!pRzD8iVsEzjwgWs*Cxo(ceqFHl{S4eab|Ad>_XW#(I7M!_x} z5pgbFzDy%ZRJjC>Gw+k)r1RO&qh>3zn?O~`LYB3kJ2Hur3Q5g-vkIPZcr`vi;?F7w z?*r(gGl~*lTsI4c2aK#qxOnOZQ&_~O#ry-)ku{3q_NcCWC7j3nR502tt&b2o+?)<< zrCnIpwr3tOt1~806@(lIE-{SCpTT@q)_0bcDX8_GhA!g|(G4H&yHT2|4MOs(!-RLWW(Hgbt;bVe!C0~cdR?0?|t!5Wr zNRaKaWq#T8P&;WmG^Yb8jmA@EyL|62ni!~BJCAo02?OzE6L?>aKhBFgcUm5rdJBqy zMM`YFc&f$6iWMaX5(z@=L;0bVrl>w=3>8hHfUMl9c_TW&{?$qF`Gt8$8|w0HHE*VQ z;OW^018CE|yKg=I8@Ddcrq!uANJ@HmkDO(<#RNA;UGb#Z0A21hIB9V;l*T#-y=oah zC2i6Bgb)Jzf}P zl=Xk^TG9e;GJNgFZZyF_49+MMub*5P%>Jh2N1s2h7?HQb{FFhky=Pb~Iu88zPMc`z+lvh-69ck1!v?SwX(r!&E4P&=ir0&Cfd<(UOw38lAgCQ)M<= zRva}}EItITv-o6Sv94cUw?VPy4_9Kop*)C7n!7XPnL!%-py1lZk)lnoAFJLy@Wom5 zKNG-4@*b&Y@Jo6lG9)4>!pv+tG9!m;)tkEwi(hGmOrb7P6r|A$a2+3W-;{nlfkr*1 z?B}7%A{2ERyF1--_P0Yyjjcc+i5K;DAU|Yk6pZ zf(eqIXlhm(%Nd;oM2cdgOD9?y?Di%m*(a&7a;pIFcNk;*VH>}7X1htv)4?em^}~t? z{4ZD&epEhl&M7m|3ZY+evk5K{+HbAUYTcU&QMmu_KhDpugn+FfXA^-zng_DQ211j? zeK*$1GtQ5+L`_7(67e@OGJwXnMXX^}j=i1T^4c0VSycSMB;MRsuoDP|916fT3+h{vvS&`95sNzawtrZxI4Yqe*D3S)J zm_pW=5`68~w|V552}t-La@kPIO!)mp@nj5=GUwNgaO~pS2tb0TluCHZPD}oLjyfl^ z%dXTEMoL{jCcbiIUWF`rj7BxisOdmgapj;2%9iKnw?qDLHv(8vVh(pej}-5@R6Q1KB2ij%(XpspFO`83=TuTmo`~?R^YX z64bS6vO|ObP}XGr4c{I)>MXq502!24W8Xo8(wuc|q?IyJR*or;}Om9Eo`d zVZ!;zd0%oChx0brTIwV~WL%)C;58Bt`Llq71x8M%(;VQuF%2RaVNtkFg?S`f$Ot(!O48Rka}zvW zU>{2a9chcls2xnV&wFm6^`sVsb)%GMkDUKv6f=Z){ zuRv2U=iNB#@$0^S#2zm64v#@)TGwS_pU0dUGIUuFX(0jTY6h^C(DraMv24GDvj3v7zL@x{Li#DOfg1HJ%pYh+>3-Jh!AON1@43VY5J)-Ijs(e zZiTfKo+t3MX5?qky?BJlqJptofbqDopcD@`=6F#_gq{W4lX#h2koinPw&Ft4uNK(A zxkO;s)Tjv52N~q!Jl(yx!WMS9=p5%aZjL(Ct+=erfa*low0ZXt?a7J|b(Du$|NJ9E zP~<>v+OR23*#@kkQN1IZEfblAtS!!<*Q-V#W{ESCx4eZP#29i^H&}}yg2yL+g^|BS zKo*!ni~Gdv*dC+>(L&Gl#E!+aAfz_W2dJv&#y5oHNhPfBkZw<#947$f(B|^HVaxvFKi3ie1&u z!r!|AtDp)Ql@Jpvg9qhLSLVc1YRNZpmjaApQlO`LW(_DabHAo-ocSq;R;xFwMj*K9 zA7fm0S~_K_CqoKgD4)7{hT`uh2~K+O;-{Chci5xWO~M%nlb}x0lnF!_j!%~%J8_?= z8rjrTRX`tQHlAgWAqW1O>;QZu9dY0fWW$(O{P?oaZtih{fJANuNfPx|yu`H8#+@>g z6)^eB(!EZj?1ao0A^2X$`>Np0QL!;H2#Gx$)2BeO3e zGB+{B+!rFT`t7~sG)~jjLE=#p#3dp-80w|-#$2H zOuvL0t>cut2`D7WJ@IXbYKb@OCj8g~qTt@b1z;xI0JxOez#SaB;3zWkx<2yZJIOI{ z7zawN%*qM5;RXa2pSmKQ{Uc-R%Z!_9-F;m^%H_o=4Z%|chm=&U5P$p5JU^Ty*q)02 zzdhWa?a(6=9WN&K+ED^h%zvzmEksJS$==D^kU_uPAZ|=zGT=z} z_vD=TE#zBvfmM<&!T~w0e7r3Or^HAo3o0xB#2NpNkYRE(LnYoc3HeU5m<~K@veor8 z2tS4U3?@|n>(|FP%QZ_loZdXYI+E<1%yw`?63VRH_QZWOl8f=kT&sG~jKn8_1D%pp zhcvhwe+GcPMjHB0_QYN*-k(k222Cy}lYD&PaZu5Syh-?bSZndSgZA$dOya$uxhpS8PUYk?Hy@(!F`73n zu@r>E(XrO(6bDx|47R6bE_n{paE!Zh06*+9=B5HJj>~n4)RSpSxkJ3O9hM*r!^+1 z1GI1$vb)aZ6LMUzHWXa>`e5FqzzImr@nTcT83FEe7V-v4lWUvk2r5IXAuY@Q@Xp)D zgRkDCyyPxAM|r_DC&oFFU-1RyN{@_$DSRh%KIu3>7%X7SLCbT}sp&=y<$eYw7?!Hh z3+S?39!GqFrT8GHbLQwMS-|~U@IFyq<)RkuNdaEtrXrmK>j)i^aYL#E#V$})#5q)> zi+==tIJR6gqn=3#Bs-B7@aGj#*!e0@F;|fJ(EGvO(0Sw}7V0&Jl=Cl(!2i5 znEQAOTDNO=!{aD2r)Ya*k9Z>ZhU6dkSdSo8Z~gwXjnFokfOn)JCYk{=0^%bOnqrm@47ux}E5;6&v79xpyfqu`aFbZT}Xk+&@ zYD<0X3LhnC|L`tn=`OhUhd*&9Q00)9gDKQ_`nZ)sTxDq9d&x|9sh~92vM2Q}YP6|{d|I5{d!S?L1tSvzLUd-%PboD8?(%6;;4JrHN+%wxbQfgE7 zkKv|2!D{qnj)5BKvjl~O$>eTQdv4`ZNDAPuefv~ccd)UG#$Nr_1(m^$WmZTzbBB2i zpf>it-P!#2MY?1%7!{F+@oFjyz(s?vmU;Z_3O3@kkf>!$jc<7gX#hQy8~cmx_-SsyLwr=EN&yY&&gWkvUR2^F;(LXnhaj7g0Hpv!cI;wmoJUwgbQU^jM2gwcTHy z{3O+IfYyO*X}O?*YA<=Lv$XJ9He7~#D;*gJlzszd+B3*F(!iqe4@qNI+D1T;G`z`& z)WWf@4`6K!VT(?}QIQ@W3<=bfDYt47PpGZZjAQCg25H{IzT`eNnZ88FmK#Jle+vox z-#byyJuT-_%0wsX*Uwv65FIG!>wYfg%@vDGFI}^ky+i?H3wOX? ztvwz8LC_L0fYX>pMWug?mvjoBE7zDIGj>G_Y=y)%ecrrG?rpt6(dN!AF{Vp-sTrUp zftB#b(k-qHG7OR#(l72+>8^=zA$PukdFV{Y6fg#~qYUuPzqHDm&X&v)LSx%_I)-?8 zHHN^*aGz8OO1F#)0rax{{uEyD4~-T%hzP%V)NK???4rE+`AEkP5+ z4{Ob1NnC&oZdhp?$#bfbJ6h$c2C*uzb%=i4>{WJq?*P!yipnw*8QCNMSM?%nDw_A_ zS^`d&+)c%`gBWgiVDCkpA)cNDsy1P)EX%ge;^!rUW&aTerwf>J;8lZaXU z<7L$O=+VFD21`deP`C_dSNad#YqErVGSJfrwm~{np>ad87Q+w= zicO}b69G;tzk!=Ts*M2rix>Bkh(x@gi-Fau440`YP)$5vS|M1itRNneh9$-rZ;b*v zQa{FndQtRbh@h2zjX%C-s^N7$0!m1}=s>$kWI4t#&{awpnvVpzccV!UT}L_W4v8QY zhEOY*?}7PI|G1`8dO&OkeWMiICUQk1CMs#j%XmEl!C?0S>b!L5P9r55(VQNJvMyd) zFR#j#bJZ0A$}j^$d8CfTL^3d_06D0J_2G z<3v(98K*#ma>g-vMpsx{Jk3MaH2%MWh2TJDm4M}awaCbT?}?~*$S-=X<(t5-5$N^o z8Ei*@lIQOXk}kn(vyv$*fC@7!jbW5B+fQI|9fI3Qlqqr^T_w5x!~t?cH{}o$x#4+E zvep){j&zvi$>F7BTA>QuOM;Cum!T@SJ_tZO>a^HkW1{R)t1uw};Y$>xZI0x{dWZb!f)pt7ry@;Z|GzDQd3=)Yaf)*_QP&S{(BSg{|w~Cv4ZNqTs zUV~l`mgvcY&%{ZVO^uVmehdeQFKrbS4aCyV4=@5t3jjid)#wHrByY*#Kys316>=}x zHv)GO!_%BT>6HJ`2KNx0;z5u*&>_aaE8t+OB?yjmb;Y6zweeSNViZW|7Tlv{Mt!$x z2b2d)$i=7vw#fX`BP5?w2l$8T?5>^tvxlKrI&yj7$^7=B+f|x3+~njB>VMNQ5I{&+Kh}P2SD-??F_jJ+S7Lq&31F*P z%u62h2u~e$jhiPLfc*+2HS55-R7k)vxkB#OstFAOeSxC7=lO#iPuHcHBHh-HA8~5N{T>pL0 zAHQffGQEtyvwB=-Dv~2`z+c);3OQP2Tl_2Im|z7GIv`zc;5>`%4X}l+DvlK%+a$}J zfQAW1pqPehS+{Aj{70WxMfFoUE#5$w1iPSF*b$*|Q-0{Wn?U$}g+5e&q#R4H5>=BI?-@OQ}v zjka#?BbOs00-y}x-jEmP=v@cn-(Lhah0h|av4+$al*%0$c64R=dq_^UG(Y&Mz;0Yg zef#xG&dl7!Wxcpc_!SmXEjfqMk3!-Se>&hr>^eNo)F_^d;QPh>K%(8;0t(&0}WEuiA_XPH-Gx1gP(kudUE z44CxMw0(21E^B zM2lm}S98Ykpq}BZV!9`QpXj^QjI!l5)9YAS9C zV^Kx^jzEN=Z$i`rs#feD7Y@qaja<^B8Pm5iunmk9^C$)iPQKjW;riiRo!glCc&b`1 znv3MZ?e`b;1~*G_=l8}yM+pHWnRb87uK*43o%CeV+4honNK-kY_y<>Y{tdcDCBIUj ztJvtPd$F6S2-S|F%@+Q5-J2=*4a=Ygn&1omw%m0lcj9reEiyar12IZqQUN+Dxt6*u z-wXifmN%Jk07CGRe^9Bfs49;Bsz&foansOK=2s>hp}6TRd*1B;&C#i0qfPi&X`Z96 zh!26WOSwct+v<8fw4ZFc!(uAqs8Jw#Nk_|PX7ntn->`RJ9dk#$1`ce2;tPqBibC7M z5#In}9i)|YVY#8`lErnpwDF7z@xh;_M91_P{G~vYC|dRv{30G) z8@oAk5b`2Gv)sD_bd+|@;@GPOz%Jh|Lu8V;lr=sExL^Gl_uJvIx5raIqW5HV5 zcHD6ygM6rPG6}=4N#N`T;O&svbkUzsQyKnFrHQW7MLZgERV|H4u4kpZMRU5hl zeAav*Jrp$=sjJZkz5yOz#-s6s>Mptc(9*+~);lxVy%;3pBOaVyuv_$|k{1xdu(`r`6f(_`Dd#2Gf=aOkxrhJaB zDIX8rpL=n6FR`I;$%I@d&9$W*XZdwMd=T2K7C&Gxksj}uNK^TJVUn7Vm_S98bE=yk z*M+txUKI2tuTtenUu5#mc$`FF2k!HVV30orR0Hb@g79`G1SyGvFPR}owkkq#l8I6N zMfrG%aDMH`XbB63hG`J<3xJgCVp1KNaHaOgJDH+p7*Y%-ur8qAF-A>r2;TkAQjY*6 z7gXaLEx;-$cbv$t_TU<+aUfoy>uadnax)``MJ}!8?h-a?8~YcY|At5nc6sm}e^YCB zMYIL9RTFSAw=t#I#FumS<$hIiJug5TjAJeCH(d}iHsAfc+~<8sHFBXISck{eTLTI( zi?zsx$OyHg-vP;L%Q%kAZ1k+TP#1l_cY^jo%yI#XM;V2J)|#jV3`q;t|MF!OcU)m6 zv?aln8X@E(P(zVLn*wP@7kby$tEW&YzxZz2`f{aqtAAZzQRDV$6|9$ed)RCaq)&bx|&>j?1-`>1yDm5WtH*+VZ-zHnpn zjvX}-&bnQp$<#FsJ-sP?+oyc`F622ZkvNq}qwMT#SG^vGH+(jdIr7De2godXvXwur zdr|NbyEkgnCehYMQ?D{hTdsS&{PEH*eF9z_VOX1>rm4~f;0gs9nd7!xnFC@f(blIJ z8(Z_1EtOqHHdj`5xz*YB|GGQ>pQ!6Nj{9vwi^#)JOai$|TXX8&9G;Ux=>(mOt)HZH zq|(4G1dbf?C3WfooTnFkK@^2CS5O-vvIdMPB8S|@>?*c`=n&*UOfC}x`~s}Ull52h zJKWvp{r;~x?Z1}XHo$&b^)9))hOA^IqLI{%^Aonl`lJy zJ1nPvu3p#BK)=Ak`Nh$Lo2}NHcIpWll3ae%bA@f9+*ZuW@$or`n)%;!St89p@aV6$!NX16sib-8x#>+7brABpbzl$NtI+IEz)^Dw5Z5cI+)eOz@>kZ zO_0By&*#%A+q^;}<3zBWFo&O`qpezo1B@l9f-T$@13QCJPnXAYOXdQ*b}bjp2vHRc zl^G$5Gmo7x4>x(zlX8cG;m-Ikm?wevo^{(O43lclzB@fV&Gr>$TGV;V_n*dG*98m= z>}65Hk4~A~Z%u~cy(5Dgq80^xVWI=an`KJ@yakP5fJ4BXU;A4pXmCjIsrH=vH4Xa@ z9B3bWi0jt%!;_vkOQ-_n6u|s~d2_?^M@=-4rECDDov0MdP%dK?=zL%6&}g8vMlnu# z5HLE!~>2qOT4>YL0WV6?T~A2M+SaxYz)6?-7qh4ikhL~n(g%mtl;Krd>!_Ww!Nvl)*c?asiLpHn0NY8f2$2m5VwY;Z>Ad#DCdy zx$i2XX`Y3IU(+*2$N+{pSCh_{mB|(N?xTpsXW7|vpY2|=)Dbo253GgBb|#Dt9vrr# z^a}KxKlSw`(C*Qce_da%n;|g{8F}y^Ze`?&qN4cybbkDuY|3y7dW@}-)LJ+#;`V_w zp%dTf9e};qSm0x&>=U0BXtg?n{uT*(Yi5*mWzmJwuSI5S7g`* zg&25?5X4El+>`dlL9QecpIn@g5BJ$%>6Jtxu$np&eWax#I*-XN4ltDs4QI=A!P^@z z-TAhv{Kin0d{LvqUNm3hgRL`#M3GP44&Ok0`BV~#N!icp6Q<59`BN^Mg!hB z%qLzCI=RklzCi|+>N~yR)xi^6tXA(5mgq8O+>}2etkosT0bQJ4)ygyxnB~4xcG}2sHJWe75Wd7W>Hb z_)gWwq*aX*G%rVF=Y=+F|8rzy^UPs()tY|7lb#B;GF5&nB^jiOEtjJk$U!bPzqY2` zbPj{r&G+zskpFi}XHx!r>d2!aY>E>}k^r)5+`UF4L0mywIiM<-h+Ggd#K834)+zU9 ze2x_~_508Jzhqdl_;jpBxGW%X4f6;(^2)}>kSB*W>Uoy;q(!Rss3wMvA(WJ$^tGV9 zFppi`{ra$Q2_3LY{jvJ#(>#)--TneOv3L`Xlm)OIS0vBwR$wlC(BisQ-WOwu7Ru0R zV)Roqr1!y@Mc_5=+sOYJ>OwgeMTuwFV(aRT|J9#Q`IUPgU352`JjV6EbGC0yuznx2 G=lH*nY$4D9 literal 612 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Or9=|Ar*{or0OmKIkF($+k@U6 zzKjwK58lTd&p5NBQ^ifwE3Q+*MT{pgIZ-+2apa8*8?mySwyCX4g^otac`iBTw(*0; zr!}4#d(<8q&X9a3)4h9#ug;a7d8hu@I&mnr2q1_<^-2@tWupQfe;2Yjr22nBNZsTO z%)DMw6CcR_nj&*p_2=5d88V@YEC0Mpl;&D-yv`(1exB07=w{Ig3toKZ@(2hCYY($= z?2^|~m>S0%zI$XLr#-Xr>x@K6x<_%mMOrKRn5Hf0Fpp?^`oh&d?XhHBk|tk*ncYI~ zli{j=e>5?QPFU3^)#GIQZGnHJ{;v;GyEjNnZxj_TlqmnBy7HCABJsO7`j?-WBKnb8 z{6b$^&M~Q;qg?f#tE~1t))QMGm_AX{bAsmUgO*z!Go>Xlb)7Ka;WPpoIWIV(H>~F{ z^Oa<)TNfMW?&$UFIUZ8-h