From 7e9e7bc8c385a88f1524bce4cf2a805da761e85a Mon Sep 17 00:00:00 2001 From: Jonathan Lurie Date: Tue, 5 Nov 2024 12:41:10 +0100 Subject: [PATCH 1/7] more robust language switching --- src/Map.ts | 57 ++++++++++++++++++++++++++++++++++++++++++---------- src/tools.ts | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/src/Map.ts b/src/Map.ts index 57ca4c9..eda67dc 100644 --- a/src/Map.ts +++ b/src/Map.ts @@ -24,7 +24,12 @@ import type { ReferenceMapStyle, MapStyleVariant } from "@maptiler/client"; import { config, MAPTILER_SESSION_ID, type SdkConfig } from "./config"; import { defaults } from "./defaults"; import { MaptilerLogoControl } from "./MaptilerLogoControl"; -import { combineTransformRequest, displayNoWebGlWarning, displayWebGLContextLostWarning } from "./tools"; +import { + changeFirstLanguage, + combineTransformRequest, + displayNoWebGlWarning, + displayWebGLContextLostWarning, +} from "./tools"; import { getBrowserLanguage, Language, type LanguageInfo } from "./language"; import { styleToStyle } from "./mapstyle"; import { MaptilerTerrainControl } from "./MaptilerTerrainControl"; @@ -190,6 +195,7 @@ export class Map extends maplibregl.Map { private terrainAnimationDuration = 1000; private monitoredStyleUrls!: Set; private styleInProcess = false; + private originalLabelStyle = new window.Map(); constructor(options: MapOptions) { displayNoWebGlWarning(options.container); @@ -730,6 +736,7 @@ export class Map extends maplibregl.Map { style: null | ReferenceMapStyle | MapStyleVariant | StyleSpecification | string, options?: StyleSwapOptions & StyleOptions, ): this { + this.originalLabelStyle.clear(); this.minimap?.setStyle(style); this.forceLanguageUpdate = true; @@ -1049,7 +1056,7 @@ export class Map extends maplibregl.Map { ]; } else if (languageNonStyle.flag === Language.AUTO.flag) { langStr = getBrowserLanguage().flag; - replacer = ["case", ["has", langStr], ["get", langStr], ["get", Language.LOCAL.flag]]; + replacer = ["coalesce", ["get", langStr], ["get", Language.LOCAL.flag]]; } // This is for using the regular names as {name} @@ -1061,11 +1068,14 @@ export class Map extends maplibregl.Map { // This section is for the regular language ISO codes else { langStr = languageNonStyle.flag; - replacer = ["case", ["has", langStr], ["get", langStr], ["get", Language.LOCAL.flag]]; + replacer = ["coalesce", ["get", langStr], ["get", Language.LOCAL.flag]]; } const { layers } = this.getStyle(); + // True if it's the first time the language is updated for the current style + const firstPassOnStyle = this.originalLabelStyle.size === 0; + for (const genericLayer of layers) { // Only symbole layer can have a layout with text-field if (genericLayer.type !== "symbol") { @@ -1102,17 +1112,42 @@ export class Map extends maplibregl.Map { continue; } - const textFieldLayoutProp = this.getLayoutProperty(id, "text-field"); + let textFieldLayoutProp: string | maplibregl.ExpressionSpecification; - // If the label is not about a name, then we don't translate it - if ( - typeof textFieldLayoutProp === "string" && - (textFieldLayoutProp.toLowerCase().includes("ref") || textFieldLayoutProp.toLowerCase().includes("housenumber")) - ) { - continue; + // Keeping a copy of the text-field sub-object as it is in the original style + if (firstPassOnStyle) { + textFieldLayoutProp = this.getLayoutProperty(id, "text-field"); + this.originalLabelStyle.set(id, textFieldLayoutProp); + } else { + textFieldLayoutProp = this.originalLabelStyle.get(id) as string | maplibregl.ExpressionSpecification; } - this.setLayoutProperty(id, "text-field", replacer); + // From this point, the value of textFieldLayoutProp is as in the original version of the style + // and never a mofified version + + // Testing the different case where the text-field property should NOT be updated: + if (typeof textFieldLayoutProp === "string") { + // If the field is a string that DOES NOT contain an opening curly bracket. + // (This happens when the label is a hardcoded string that does not refer to a property) + if (!textFieldLayoutProp.includes("{")) { + continue; + } + + // If the text field does not contain the "name" substring. + // This happens when dealing with {ref}, {housenumber}, {height}, etc. + if (!textFieldLayoutProp.includes("name")) { + continue; + } + + this.setLayoutProperty(id, "text-field", replacer); + } + + // The value of text-field is an object + else { + const newReplacer = changeFirstLanguage(textFieldLayoutProp, replacer); + console.log("New replacer:", newReplacer); + this.setLayoutProperty(id, "text-field", newReplacer); + } } } diff --git a/src/tools.ts b/src/tools.ts index dc25885..7d7be3c 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -226,3 +226,40 @@ export function displayWebGLContextLostWarning(container: HTMLElement | string) actualContainer.appendChild(errorMessageDiv); // throw new Error(webglError); } + +/** + * Return true if the provided piece of style expression has the form ["get", "name:XX"] + * with XX benig a 2-letter is code for languages + */ +function isGetNameLanguage(subExpr: unknown): boolean { + if (!Array.isArray(subExpr)) return false; + if (subExpr.length !== 2) return false; + if (subExpr[0] !== "get") return false; + if (typeof subExpr[1] !== "string") return false; + if (!subExpr[1].startsWith("name:")) return false; + + return true; +} + +export function changeFirstLanguage( + origExpr: maplibregl.ExpressionSpecification, + replacer: maplibregl.ExpressionSpecification | string, +): maplibregl.ExpressionSpecification { + const expr = structuredClone(origExpr) as maplibregl.ExpressionSpecification; + + const exploreNode = (subExpr: maplibregl.ExpressionSpecification | string) => { + if (typeof subExpr === "string") return; + + for (let i = 0; i < subExpr.length; i += 1) { + if (isGetNameLanguage(subExpr[i])) { + subExpr[i] = structuredClone(replacer); + } else { + exploreNode(subExpr[i] as maplibregl.ExpressionSpecification | string); + } + } + }; + + exploreNode(expr); + + return expr; +} From f5401066de2ebc1272f88ae81fc905a0a2de0456 Mon Sep 17 00:00:00 2001 From: Jonathan Lurie Date: Tue, 5 Nov 2024 14:58:22 +0100 Subject: [PATCH 2/7] LOCAL now working and changelog --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- src/Map.ts | 5 ++--- src/tools.ts | 7 ++++++- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61ab3ed..e420ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # MapTiler SDK Changelog +## 2.4.2 +### Bug Fixes +- The language switching is now more robust and preserves the original formatting from the style (`Map.setPrimareyLangage()`) + ## 2.4.1 ### Bug Fixes - The class `AJAXError` is now imported as part of the `maplibregl` namespace (CommonJS limitation from Maplibre GL JS) (https://github.com/maptiler/maptiler-sdk-js/pull/129) diff --git a/package-lock.json b/package-lock.json index 59674ff..2b2469f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@maptiler/sdk", - "version": "2.4.1", + "version": "2.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@maptiler/sdk", - "version": "2.3.1", + "version": "2.4.2", "license": "BSD-3-Clause", "dependencies": { "@maplibre/maplibre-gl-style-spec": "^20.3.1", diff --git a/package.json b/package.json index 3169cda..3c5e49d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@maptiler/sdk", - "version": "2.4.1", + "version": "2.4.2", "description": "The Javascript & TypeScript map SDK tailored for MapTiler Cloud", "module": "dist/maptiler-sdk.mjs", "types": "dist/maptiler-sdk.d.ts", diff --git a/src/Map.ts b/src/Map.ts index eda67dc..7485251 100644 --- a/src/Map.ts +++ b/src/Map.ts @@ -1010,7 +1010,7 @@ export class Map extends maplibregl.Map { let langStr = Language.LOCAL.flag; // will be overwritten below - let replacer: ExpressionSpecification | string = `{${langStr}}`; + let replacer: ExpressionSpecification | string = ["get", langStr]; if (languageNonStyle.flag === Language.VISITOR.flag) { langStr = getBrowserLanguage().flag; @@ -1062,7 +1062,7 @@ export class Map extends maplibregl.Map { // This is for using the regular names as {name} else if (languageNonStyle === Language.LOCAL) { langStr = Language.LOCAL.flag; - replacer = `{${langStr}}`; + replacer = ["get", langStr]; } // This section is for the regular language ISO codes @@ -1145,7 +1145,6 @@ export class Map extends maplibregl.Map { // The value of text-field is an object else { const newReplacer = changeFirstLanguage(textFieldLayoutProp, replacer); - console.log("New replacer:", newReplacer); this.setLayoutProperty(id, "text-field", newReplacer); } } diff --git a/src/tools.ts b/src/tools.ts index 7d7be3c..8f7be33 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -241,6 +241,12 @@ function isGetNameLanguage(subExpr: unknown): boolean { return true; } +/** + * In a text-field style property (as an object, not a string) the langages that are specified as + * ["get", "name:XX"] are replaced by the proved replacer (also an object). + * This replacement happened regardless of how deep in the object the flag is. + * Note that it does not replace the occurences of ["get", "name"] (local names) + */ export function changeFirstLanguage( origExpr: maplibregl.ExpressionSpecification, replacer: maplibregl.ExpressionSpecification | string, @@ -260,6 +266,5 @@ export function changeFirstLanguage( }; exploreNode(expr); - return expr; } From 71216af365f4b63a7e1e714b24f3043e1b5ef50b Mon Sep 17 00:00:00 2001 From: Jonathan Lurie Date: Tue, 5 Nov 2024 15:01:31 +0100 Subject: [PATCH 3/7] changelog with link --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e420ec1..ac4a0e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.4.2 ### Bug Fixes -- The language switching is now more robust and preserves the original formatting from the style (`Map.setPrimareyLangage()`) +- The language switching is now more robust and preserves the original formatting from the style (`Map.setPrimareyLangage()`) (https://github.com/maptiler/maptiler-sdk-js/pull/134) ## 2.4.1 ### Bug Fixes From 67562dc2bb99578cec24572149631d73eec7b1aa Mon Sep 17 00:00:00 2001 From: Jonathan Lurie Date: Wed, 6 Nov 2024 13:58:29 +0100 Subject: [PATCH 4/7] more robust language switching --- CHANGELOG.md | 2 +- src/Map.ts | 27 ++++++++++++++++----------- src/tools.ts | 28 ++++++++++++++++++++++++++++ tsconfig.json | 4 ++-- 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac4a0e3..eccd094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.4.2 ### Bug Fixes -- The language switching is now more robust and preserves the original formatting from the style (`Map.setPrimareyLangage()`) (https://github.com/maptiler/maptiler-sdk-js/pull/134) +- The language switching is now more robust and preserves the original formatting from the style (`Map.setPrimaryLangage()`) (https://github.com/maptiler/maptiler-sdk-js/pull/134) ## 2.4.1 ### Bug Fixes diff --git a/src/Map.ts b/src/Map.ts index 7485251..d182775 100644 --- a/src/Map.ts +++ b/src/Map.ts @@ -26,9 +26,11 @@ import { defaults } from "./defaults"; import { MaptilerLogoControl } from "./MaptilerLogoControl"; import { changeFirstLanguage, + checkNamePattern, combineTransformRequest, displayNoWebGlWarning, displayWebGLContextLostWarning, + replaceLanguage, } from "./tools"; import { getBrowserLanguage, Language, type LanguageInfo } from "./language"; import { styleToStyle } from "./mapstyle"; @@ -1127,19 +1129,22 @@ export class Map extends maplibregl.Map { // Testing the different case where the text-field property should NOT be updated: if (typeof textFieldLayoutProp === "string") { - // If the field is a string that DOES NOT contain an opening curly bracket. - // (This happens when the label is a hardcoded string that does not refer to a property) - if (!textFieldLayoutProp.includes("{")) { - continue; - } + const { contains, exactMatch } = checkNamePattern(textFieldLayoutProp); - // If the text field does not contain the "name" substring. - // This happens when dealing with {ref}, {housenumber}, {height}, etc. - if (!textFieldLayoutProp.includes("name")) { - continue; - } + // If the current text-fiels does not contain any "{name:xx}" pattern + if (!contains) continue; - this.setLayoutProperty(id, "text-field", replacer); + // In case of an exact match, we replace by an object representation of the label + if (exactMatch) { + this.setLayoutProperty(id, "text-field", replacer); + } else { + // In case of a non-exact match (such as "foo {name:xx} bar") + // we create a "concat" object expresion composed of the original elements with new replacer + // in-betweem + const newReplacer = replaceLanguage(textFieldLayoutProp, replacer); + + this.setLayoutProperty(id, "text-field", newReplacer); + } } // The value of text-field is an object diff --git a/src/tools.ts b/src/tools.ts index 8f7be33..2acbc3d 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -268,3 +268,31 @@ export function changeFirstLanguage( exploreNode(expr); return expr; } + +/** + * Tst if a string matches the pattern "{name:xx}" in a exact way or is a loose way (such as "foo {name:xx}") + */ +export function checkNamePattern(str: string): { contains: boolean; exactMatch: boolean } { + const regex = /\{name:[a-zA-Z]{2}\}/; + return { + contains: regex.test(str), + exactMatch: new RegExp(`^${regex.source}$`).test(str), + }; +} + +/** + * Replaces the occurences of {name:xx} in a string by a provided object-expression to return a concat object expression + */ +export function replaceLanguage( + origLang: string, + newLang: maplibregl.ExpressionSpecification, +): maplibregl.ExpressionSpecification { + const elementsToConcat = origLang.split(/\{name:[a-zA-Z]{2}\}/); + + const allElements = elementsToConcat.flatMap((item, i) => + i === elementsToConcat.length - 1 ? [item] : [item, newLang], + ); + + const expr = ["concat", ...allElements] as maplibregl.ExpressionSpecification; + return expr; +} diff --git a/tsconfig.json b/tsconfig.json index 879d3bb..14e795f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,10 +2,10 @@ "compilerOptions": { "baseUrl": "src", "moduleResolution": "Node", - "target": "ES2020", + "target": "es2021", "useDefineForClassFields": true, "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["es2021", "DOM", "DOM.Iterable"], "skipLibCheck": true, /* Bundler mode */ From 74d2302aa426d567ea6387495a7a3142d63d85f7 Mon Sep 17 00:00:00 2001 From: Jonathan Lurie Date: Fri, 8 Nov 2024 12:25:04 +0100 Subject: [PATCH 5/7] language detection for name:xx more robust --- src/tools.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools.ts b/src/tools.ts index 2acbc3d..1afa4d3 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -273,7 +273,7 @@ export function changeFirstLanguage( * Tst if a string matches the pattern "{name:xx}" in a exact way or is a loose way (such as "foo {name:xx}") */ export function checkNamePattern(str: string): { contains: boolean; exactMatch: boolean } { - const regex = /\{name:[a-zA-Z]{2}\}/; + const regex = /\{name:\S+\}/; return { contains: regex.test(str), exactMatch: new RegExp(`^${regex.source}$`).test(str), @@ -287,7 +287,7 @@ export function replaceLanguage( origLang: string, newLang: maplibregl.ExpressionSpecification, ): maplibregl.ExpressionSpecification { - const elementsToConcat = origLang.split(/\{name:[a-zA-Z]{2}\}/); + const elementsToConcat = origLang.split(/\{name:\S+\}/); const allElements = elementsToConcat.flatMap((item, i) => i === elementsToConcat.length - 1 ? [item] : [item, newLang], From ae7e8d2e2fc61c47ce4b4ca9346e9bc7282d7b74 Mon Sep 17 00:00:00 2001 From: Jonathan Lurie Date: Mon, 11 Nov 2024 17:58:35 +0100 Subject: [PATCH 6/7] add detection if style is localized --- src/Map.ts | 12 +++- src/tools.ts | 151 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 157 insertions(+), 6 deletions(-) diff --git a/src/Map.ts b/src/Map.ts index d182775..56b479f 100644 --- a/src/Map.ts +++ b/src/Map.ts @@ -28,6 +28,7 @@ import { changeFirstLanguage, checkNamePattern, combineTransformRequest, + computeLabelsLocalizationMetrics, displayNoWebGlWarning, displayWebGLContextLostWarning, replaceLanguage, @@ -198,6 +199,7 @@ export class Map extends maplibregl.Map { private monitoredStyleUrls!: Set; private styleInProcess = false; private originalLabelStyle = new window.Map(); + private isStyleLocalized = false; constructor(options: MapOptions) { displayNoWebGlWarning(options.container); @@ -1012,7 +1014,7 @@ export class Map extends maplibregl.Map { let langStr = Language.LOCAL.flag; // will be overwritten below - let replacer: ExpressionSpecification | string = ["get", langStr]; + let replacer: ExpressionSpecification = ["get", langStr]; if (languageNonStyle.flag === Language.VISITOR.flag) { langStr = getBrowserLanguage().flag; @@ -1078,6 +1080,14 @@ export class Map extends maplibregl.Map { // True if it's the first time the language is updated for the current style const firstPassOnStyle = this.originalLabelStyle.size === 0; + // Analisis on all the label layers to check the languages being used + if (firstPassOnStyle) { + const labelsLocalizationMetrics = computeLabelsLocalizationMetrics(layers, this); + this.isStyleLocalized = Object.keys(labelsLocalizationMetrics.localized).length > 0; + console.log("this.isStyleLocalized", this.isStyleLocalized); + + } + for (const genericLayer of layers) { // Only symbole layer can have a layout with text-field if (genericLayer.type !== "symbol") { diff --git a/src/tools.ts b/src/tools.ts index 1afa4d3..9d198d5 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -1,9 +1,10 @@ import maplibregl from "maplibre-gl"; -import type { RequestParameters, ResourceType, RequestTransformFunction } from "maplibre-gl"; +import type { RequestParameters, ResourceType, RequestTransformFunction, SymbolLayerSpecification } from "maplibre-gl"; import { defaults } from "./defaults"; import { config } from "./config"; import { MAPTILER_SESSION_ID } from "./config"; import { localCacheTransformRequest } from "./caching"; +import { Map as MapSDK } from "./Map"; export function enableRTL() { // Prevent this from running server side @@ -229,7 +230,7 @@ export function displayWebGLContextLostWarning(container: HTMLElement | string) /** * Return true if the provided piece of style expression has the form ["get", "name:XX"] - * with XX benig a 2-letter is code for languages + * with XX being a 2-letter is code for languages */ function isGetNameLanguage(subExpr: unknown): boolean { if (!Array.isArray(subExpr)) return false; @@ -242,14 +243,14 @@ function isGetNameLanguage(subExpr: unknown): boolean { } /** - * In a text-field style property (as an object, not a string) the langages that are specified as + * In a text-field style property (as an object, not a string) the languages that are specified as * ["get", "name:XX"] are replaced by the proved replacer (also an object). * This replacement happened regardless of how deep in the object the flag is. * Note that it does not replace the occurences of ["get", "name"] (local names) */ export function changeFirstLanguage( origExpr: maplibregl.ExpressionSpecification, - replacer: maplibregl.ExpressionSpecification | string, + replacer: maplibregl.ExpressionSpecification, ): maplibregl.ExpressionSpecification { const expr = structuredClone(origExpr) as maplibregl.ExpressionSpecification; @@ -265,12 +266,17 @@ export function changeFirstLanguage( } }; + // The provided expression could be directly a ["get", "name:xx"] + if (isGetNameLanguage(expr)) { + return replacer; + } + exploreNode(expr); return expr; } /** - * Tst if a string matches the pattern "{name:xx}" in a exact way or is a loose way (such as "foo {name:xx}") + * Test if a string matches the pattern "{name:xx}" in a exact way or is a loose way (such as "foo {name:xx}") */ export function checkNamePattern(str: string): { contains: boolean; exactMatch: boolean } { const regex = /\{name:\S+\}/; @@ -296,3 +302,138 @@ export function replaceLanguage( const expr = ["concat", ...allElements] as maplibregl.ExpressionSpecification; return expr; } + + +/** + * Find languages used in string label definition. + * The returned array contains languages such as "en", "fr" but + * can also contain null that stand for the use of {name} + */ +export function findLanguageStr(str: string): Array { + const regex = /\{name(?:\:(?\S+))?\}/g; + const languageUsed = [] as Array; + + while (true) { + const match = regex.exec(str); + if (!match) break; + + // The is a match + const language = match.groups?.language ?? null; + + // The language is non-null if provided {name:xx} + // but if provided {name} then language will be null + languageUsed.push(language); + } + return languageUsed; +} + + +function isGetNameLanguageAndFind(subExpr: unknown): {isLanguage: boolean, localization: string | null} | null { + // Not language expression + if (!Array.isArray(subExpr)) return null + if (subExpr.length !== 2) return null; + if (subExpr[0] !== "get") return null; + if (typeof subExpr[1] !== "string") return null; + + // Is non localized language + if (subExpr[1].trim() === "name") { + return { + isLanguage: true, + localization: null, + }; + } + + // Is a localized language + if (subExpr[1].trim().startsWith("name:")) { + return { + isLanguage: true, + localization: subExpr[1].trim().split(":").pop() as string, + } + } + + return null; +} + + +/** + * Find languages used in object label definition. + * The returned array contains languages such as "en", "fr" but + * can also contain null that stand for the use of {name} + */ +export function findLanguageObj(origExpr: maplibregl.ExpressionSpecification): Array { + const languageUsed = [] as Array; + const expr = structuredClone(origExpr) as maplibregl.ExpressionSpecification; + + const exploreNode = (subExpr: maplibregl.ExpressionSpecification | string | Array) => { + if (typeof subExpr === "string") return; + + for (let i = 0; i < subExpr.length; i += 1) { + const result = isGetNameLanguageAndFind(subExpr[i]); + if (result) { + languageUsed.push(result.localization); + } else { + exploreNode(subExpr[i] as maplibregl.ExpressionSpecification | string); + } + } + }; + + exploreNode([expr]); + return languageUsed; +} + + +export function computeLabelsLocalizationMetrics(layers: maplibregl.LayerSpecification[], map: MapSDK): {unlocalized: number, localized: {[k: string]: number}} { + const languages: Array[] = []; + + for (const genericLayer of layers) { + // Only symbole layer can have a layout with text-field + if (genericLayer.type !== "symbol") { + continue; + } + + const layer = genericLayer as SymbolLayerSpecification; + const { id, layout } = layer; + + if (!layout) { + continue; + } + + if (!("text-field" in layout)) { + continue; + } + + const textFieldLayoutProp: string | maplibregl.ExpressionSpecification = map.getLayoutProperty(id, "text-field"); + + if (!textFieldLayoutProp) { + continue; + } + + if (typeof textFieldLayoutProp === "string") { + const l = findLanguageStr(textFieldLayoutProp); + languages.push(l); + } else { + const l = findLanguageObj(textFieldLayoutProp); + languages.push(l); + } + } + + const flatLanguages = languages.flat(); + const localizationMetrics: {unlocalized: number, localized: {[k: string]: number}} = { + unlocalized: 0, + localized: {}, + }; + + for (const lang of flatLanguages) { + if (lang === null) { + localizationMetrics.unlocalized += 1; + } else { + if (!(lang in localizationMetrics.localized)) { + localizationMetrics.localized[lang] = 0; + } + localizationMetrics.localized[lang] += 1; + } + + } + console.log(localizationMetrics); + return localizationMetrics; +} \ No newline at end of file From fb1d86b78b302531f6cee70158049f8159589f0b Mon Sep 17 00:00:00 2001 From: Jonathan Lurie Date: Tue, 12 Nov 2024 10:48:36 +0100 Subject: [PATCH 7/7] Language switch now compatible with style with local language --- src/Map.ts | 15 +++++++++------ src/tools.ts | 51 ++++++++++++++++++++++++++++----------------------- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/Map.ts b/src/Map.ts index 56b479f..da04791 100644 --- a/src/Map.ts +++ b/src/Map.ts @@ -1084,8 +1084,6 @@ export class Map extends maplibregl.Map { if (firstPassOnStyle) { const labelsLocalizationMetrics = computeLabelsLocalizationMetrics(layers, this); this.isStyleLocalized = Object.keys(labelsLocalizationMetrics.localized).length > 0; - console.log("this.isStyleLocalized", this.isStyleLocalized); - } for (const genericLayer of layers) { @@ -1139,7 +1137,12 @@ export class Map extends maplibregl.Map { // Testing the different case where the text-field property should NOT be updated: if (typeof textFieldLayoutProp === "string") { - const { contains, exactMatch } = checkNamePattern(textFieldLayoutProp); + // When the original style is localized (this.isStyleLocalized is true), we do not modify the {name} because they are + // very likely to be only fallbacks. + // When the original style is not localized (this.isStyleLocalized is false), the occurences of "{name}" + // should be replaced by localized versions with fallback to local language. + + const { contains, exactMatch } = checkNamePattern(textFieldLayoutProp, this.isStyleLocalized); // If the current text-fiels does not contain any "{name:xx}" pattern if (!contains) continue; @@ -1148,10 +1151,10 @@ export class Map extends maplibregl.Map { if (exactMatch) { this.setLayoutProperty(id, "text-field", replacer); } else { - // In case of a non-exact match (such as "foo {name:xx} bar") + // In case of a non-exact match (such as "foo {name:xx} bar" or "foo {name} bar", depending on localization) // we create a "concat" object expresion composed of the original elements with new replacer // in-betweem - const newReplacer = replaceLanguage(textFieldLayoutProp, replacer); + const newReplacer = replaceLanguage(textFieldLayoutProp, replacer, this.isStyleLocalized); this.setLayoutProperty(id, "text-field", newReplacer); } @@ -1159,7 +1162,7 @@ export class Map extends maplibregl.Map { // The value of text-field is an object else { - const newReplacer = changeFirstLanguage(textFieldLayoutProp, replacer); + const newReplacer = changeFirstLanguage(textFieldLayoutProp, replacer, this.isStyleLocalized); this.setLayoutProperty(id, "text-field", newReplacer); } } diff --git a/src/tools.ts b/src/tools.ts index 9d198d5..7a8c2bc 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -4,7 +4,7 @@ import { defaults } from "./defaults"; import { config } from "./config"; import { MAPTILER_SESSION_ID } from "./config"; import { localCacheTransformRequest } from "./caching"; -import { Map as MapSDK } from "./Map"; +import type { Map as MapSDK } from "./Map"; export function enableRTL() { // Prevent this from running server side @@ -232,12 +232,13 @@ export function displayWebGLContextLostWarning(container: HTMLElement | string) * Return true if the provided piece of style expression has the form ["get", "name:XX"] * with XX being a 2-letter is code for languages */ -function isGetNameLanguage(subExpr: unknown): boolean { +function isGetNameLanguage(subExpr: unknown, localized: boolean): boolean { if (!Array.isArray(subExpr)) return false; if (subExpr.length !== 2) return false; if (subExpr[0] !== "get") return false; if (typeof subExpr[1] !== "string") return false; - if (!subExpr[1].startsWith("name:")) return false; + if (localized && !subExpr[1].startsWith("name:")) return false; + if (!localized && subExpr[1] !== "name") return false; return true; } @@ -251,6 +252,7 @@ function isGetNameLanguage(subExpr: unknown): boolean { export function changeFirstLanguage( origExpr: maplibregl.ExpressionSpecification, replacer: maplibregl.ExpressionSpecification, + localized: boolean, ): maplibregl.ExpressionSpecification { const expr = structuredClone(origExpr) as maplibregl.ExpressionSpecification; @@ -258,7 +260,7 @@ export function changeFirstLanguage( if (typeof subExpr === "string") return; for (let i = 0; i < subExpr.length; i += 1) { - if (isGetNameLanguage(subExpr[i])) { + if (isGetNameLanguage(subExpr[i], localized)) { subExpr[i] = structuredClone(replacer); } else { exploreNode(subExpr[i] as maplibregl.ExpressionSpecification | string); @@ -267,7 +269,7 @@ export function changeFirstLanguage( }; // The provided expression could be directly a ["get", "name:xx"] - if (isGetNameLanguage(expr)) { + if (isGetNameLanguage(expr, localized)) { return replacer; } @@ -276,10 +278,12 @@ export function changeFirstLanguage( } /** - * Test if a string matches the pattern "{name:xx}" in a exact way or is a loose way (such as "foo {name:xx}") + * If `localized` is `true`, it checks for the pattern "{name:xx}" (with "xx" being a language iso string). + * If `localized` is `false`, it check for {name}. + * In a exact way or is a loose way (such as "foo {name:xx}" or "foo {name} bar") */ -export function checkNamePattern(str: string): { contains: boolean; exactMatch: boolean } { - const regex = /\{name:\S+\}/; +export function checkNamePattern(str: string, localized: boolean): { contains: boolean; exactMatch: boolean } { + const regex = localized ? /\{name:\S+\}/ : /\{name\}/; return { contains: regex.test(str), exactMatch: new RegExp(`^${regex.source}$`).test(str), @@ -292,8 +296,10 @@ export function checkNamePattern(str: string): { contains: boolean; exactMatch: export function replaceLanguage( origLang: string, newLang: maplibregl.ExpressionSpecification, + localized: boolean, ): maplibregl.ExpressionSpecification { - const elementsToConcat = origLang.split(/\{name:\S+\}/); + const regex = localized ? /\{name:\S+\}/ : /\{name\}/; + const elementsToConcat = origLang.split(regex); const allElements = elementsToConcat.flatMap((item, i) => i === elementsToConcat.length - 1 ? [item] : [item, newLang], @@ -303,7 +309,6 @@ export function replaceLanguage( return expr; } - /** * Find languages used in string label definition. * The returned array contains languages such as "en", "fr" but @@ -327,10 +332,9 @@ export function findLanguageStr(str: string): Array { return languageUsed; } - -function isGetNameLanguageAndFind(subExpr: unknown): {isLanguage: boolean, localization: string | null} | null { +function isGetNameLanguageAndFind(subExpr: unknown): { isLanguage: boolean; localization: string | null } | null { // Not language expression - if (!Array.isArray(subExpr)) return null + if (!Array.isArray(subExpr)) return null; if (subExpr.length !== 2) return null; if (subExpr[0] !== "get") return null; if (typeof subExpr[1] !== "string") return null; @@ -348,13 +352,12 @@ function isGetNameLanguageAndFind(subExpr: unknown): {isLanguage: boolean, local return { isLanguage: true, localization: subExpr[1].trim().split(":").pop() as string, - } + }; } return null; } - /** * Find languages used in object label definition. * The returned array contains languages such as "en", "fr" but @@ -364,7 +367,9 @@ export function findLanguageObj(origExpr: maplibregl.ExpressionSpecification): A const languageUsed = [] as Array; const expr = structuredClone(origExpr) as maplibregl.ExpressionSpecification; - const exploreNode = (subExpr: maplibregl.ExpressionSpecification | string | Array) => { + const exploreNode = ( + subExpr: maplibregl.ExpressionSpecification | string | Array, + ) => { if (typeof subExpr === "string") return; for (let i = 0; i < subExpr.length; i += 1) { @@ -381,9 +386,11 @@ export function findLanguageObj(origExpr: maplibregl.ExpressionSpecification): A return languageUsed; } - -export function computeLabelsLocalizationMetrics(layers: maplibregl.LayerSpecification[], map: MapSDK): {unlocalized: number, localized: {[k: string]: number}} { - const languages: Array[] = []; +export function computeLabelsLocalizationMetrics( + layers: maplibregl.LayerSpecification[], + map: MapSDK, +): { unlocalized: number; localized: { [k: string]: number } } { + const languages: Array[] = []; for (const genericLayer of layers) { // Only symbole layer can have a layout with text-field @@ -418,7 +425,7 @@ export function computeLabelsLocalizationMetrics(layers: maplibregl.LayerSpecifi } const flatLanguages = languages.flat(); - const localizationMetrics: {unlocalized: number, localized: {[k: string]: number}} = { + const localizationMetrics: { unlocalized: number; localized: { [k: string]: number } } = { unlocalized: 0, localized: {}, }; @@ -432,8 +439,6 @@ export function computeLabelsLocalizationMetrics(layers: maplibregl.LayerSpecifi } localizationMetrics.localized[lang] += 1; } - } - console.log(localizationMetrics); return localizationMetrics; -} \ No newline at end of file +}