@panoramax/web-viewer 3.0.2-develop-a8ea8e60-develop-f1bb641f → 3.1.0-develop-428bc81e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/docs/05_Compatibility.md +14 -0
- package/package.json +1 -1
- package/src/utils/I18n.js +13 -6
- package/src/utils/Map.js +42 -1
- package/src/viewer/URLHash.js +102 -6
- package/src/viewer/Widgets.js +2 -1
- package/tests/components/Map.test.js +2 -1
- package/tests/utils/I18n.test.js +84 -1
package/docs/05_Compatibility.md
CHANGED
|
@@ -83,3 +83,17 @@ A supplementary layer _grid_ can be made available for low-zoom overview:
|
|
|
83
83
|
|
|
84
84
|
- Available on zoom levels < 6
|
|
85
85
|
- Available properties: `id` (grid cell ID), `nb_pictures` (amount of pictures), `coef` (value from 0 to 1, relative quantity of available pictures)
|
|
86
|
+
|
|
87
|
+
### Labels translation
|
|
88
|
+
|
|
89
|
+
If your vector tiles support multiple languages, you can set in your `style.json` the list of supported languages :
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"metadata": {
|
|
94
|
+
"panoramax:locales": ["fr", "en", "latin"]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The viewer will try to find the best matching `name:LANG` property according to user browser settings.
|
package/package.json
CHANGED
package/src/utils/I18n.js
CHANGED
|
@@ -10,7 +10,13 @@ const TRANSLATIONS = {
|
|
|
10
10
|
"de": T_de, "en": T_en, "es": T_es, "fr": T_fr, "hu": T_hu, "zh_Hant": T_zh_Hant,
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Find best matching language regarding of list of supported languages and browser accepted languages
|
|
15
|
+
* @param {str[]} supportedTranslations List of supported languages
|
|
16
|
+
* @param {str} fallback The fallback language
|
|
17
|
+
* @returns The best matching language
|
|
18
|
+
*/
|
|
19
|
+
export function autoDetectLocale(supportedTranslations, fallback) { // eslint-ignore import/no-unused-modules
|
|
14
20
|
for (const navigatorLang of window.navigator.languages) {
|
|
15
21
|
let language = navigatorLang;
|
|
16
22
|
// Convert browser code to weblate code
|
|
@@ -30,13 +36,14 @@ const autoDetectLocale = () => {
|
|
|
30
36
|
}
|
|
31
37
|
break;
|
|
32
38
|
}
|
|
33
|
-
const pair =
|
|
39
|
+
const pair = supportedTranslations.find((pair) => pair === language);
|
|
34
40
|
if (pair) {
|
|
35
|
-
return pair
|
|
41
|
+
return pair;
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
|
-
return
|
|
39
|
-
}
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
/**
|
|
41
48
|
* Get text labels translations in given language
|
|
42
49
|
*
|
|
@@ -50,7 +57,7 @@ export function getTranslations(lang = "") {
|
|
|
50
57
|
|
|
51
58
|
// No specific lang set -> use browser lang
|
|
52
59
|
if(!lang) {
|
|
53
|
-
lang = autoDetectLocale();
|
|
60
|
+
lang = autoDetectLocale(Object.keys(TRANSLATIONS), FALLBACK_LOCALE);
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
// Lang exists -> send it
|
package/src/utils/Map.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import maplibregl from "!maplibre-gl";
|
|
3
3
|
import LoaderImg from "../img/marker.svg";
|
|
4
4
|
import { COLORS } from "./Utils";
|
|
5
|
+
import { autoDetectLocale } from "./I18n";
|
|
5
6
|
|
|
6
7
|
export const DEFAULT_TILES = "https://panoramax.openstreetmap.fr/pmtiles/basic.json";
|
|
7
8
|
export const RASTER_LAYER_ID = "gvs-aerial";
|
|
@@ -152,7 +153,47 @@ export function combineStyles(parent, options) {
|
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
});
|
|
155
|
-
|
|
156
|
+
|
|
157
|
+
// TODO : remove override once available in default Panoramax style
|
|
158
|
+
if(!style.metadata["panoramax:locales"]) {
|
|
159
|
+
style.metadata["panoramax:locales"] = ["fr", "en", "de", "es", "ru", "pt", "zh", "hi", "latin"];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Override labels to use appropriate language
|
|
163
|
+
if(style.metadata["panoramax:locales"]) {
|
|
164
|
+
const prefLang = autoDetectLocale(style.metadata["panoramax:locales"], "latin");
|
|
165
|
+
style.layers.forEach(l => {
|
|
166
|
+
if(isLabelLayer(l) && l.layout["text-field"].includes("name:latin")) {
|
|
167
|
+
l.layout["text-field"] = [
|
|
168
|
+
"coalesce",
|
|
169
|
+
["get", `name:${prefLang}`],
|
|
170
|
+
["get", "name:latin"],
|
|
171
|
+
["get", "name"]
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Fix for capital cities
|
|
178
|
+
const citiesLayer = style.layers.find(l => l.id == "place_label_city");
|
|
179
|
+
let capitalLayer = style.layers.find(l => l.id == "place_label_capital");
|
|
180
|
+
if(citiesLayer && !capitalLayer) {
|
|
181
|
+
// Create capital layer from original city style
|
|
182
|
+
capitalLayer = JSON.parse(JSON.stringify(citiesLayer));
|
|
183
|
+
capitalLayer.id = "place_label_capital";
|
|
184
|
+
capitalLayer.filter.push(["<=", "capital", 2]);
|
|
185
|
+
|
|
186
|
+
// Edit original city to make it less import
|
|
187
|
+
citiesLayer.filter.push([">", "capital", 2]);
|
|
188
|
+
citiesLayer.paint = {
|
|
189
|
+
"text-color": "hsl(0,0%,15%)",
|
|
190
|
+
"text-halo-blur": 0.5,
|
|
191
|
+
"text-halo-color": "hsl(0,0%,100%)",
|
|
192
|
+
"text-halo-width": 0.8,
|
|
193
|
+
};
|
|
194
|
+
style.layers.push(capitalLayer);
|
|
195
|
+
}
|
|
196
|
+
|
|
156
197
|
return style;
|
|
157
198
|
}
|
|
158
199
|
|
package/src/viewer/URLHash.js
CHANGED
|
@@ -57,11 +57,11 @@ export default class URLHash extends EventTarget {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
61
|
-
* @
|
|
60
|
+
* Compute next hash parts
|
|
61
|
+
* @returns {object} Hash parameters
|
|
62
|
+
* @private
|
|
62
63
|
*/
|
|
63
|
-
|
|
64
|
-
let hash = "";
|
|
64
|
+
_getHashParts() {
|
|
65
65
|
let hashParts = {};
|
|
66
66
|
|
|
67
67
|
if(typeof this._viewer.psv.getTransitionDuration() == "number") {
|
|
@@ -103,8 +103,17 @@ export default class URLHash extends EventTarget {
|
|
|
103
103
|
else {
|
|
104
104
|
hashParts.map = "none";
|
|
105
105
|
}
|
|
106
|
+
return hashParts;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get the hash string with current map/psv parameters
|
|
111
|
+
* @return {string} The hash, starting with #
|
|
112
|
+
*/
|
|
113
|
+
getHashString() {
|
|
114
|
+
let hash = "";
|
|
106
115
|
|
|
107
|
-
Object.entries(
|
|
116
|
+
Object.entries(this._getHashParts())
|
|
108
117
|
.sort((a,b) => a[0].localeCompare(b[0]))
|
|
109
118
|
.forEach(entry => {
|
|
110
119
|
let [ hashName, value ] = entry;
|
|
@@ -144,6 +153,63 @@ export default class URLHash extends EventTarget {
|
|
|
144
153
|
.forEach(part => {
|
|
145
154
|
keyvals[part[0]] = part[1];
|
|
146
155
|
});
|
|
156
|
+
|
|
157
|
+
// If hash is compressed
|
|
158
|
+
if(keyvals.s) {
|
|
159
|
+
const shortVals = Object.fromEntries(
|
|
160
|
+
keyvals.s
|
|
161
|
+
.split("|")
|
|
162
|
+
.map(kv => [kv[0], kv.substring(1)])
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
keyvals = {};
|
|
166
|
+
|
|
167
|
+
// Used letters: b c d e f k m n p s t u v
|
|
168
|
+
// Focus
|
|
169
|
+
if(shortVals.f === "m") { keyvals.focus = "map"; }
|
|
170
|
+
else if(shortVals.f === "p") { keyvals.focus = "pic"; }
|
|
171
|
+
else if(shortVals.f === "t") { keyvals.focus = "meta"; }
|
|
172
|
+
|
|
173
|
+
// Speed
|
|
174
|
+
if(shortVals.s !== "") { keyvals.speed = parseFloat(shortVals.s) * 100; }
|
|
175
|
+
|
|
176
|
+
// Nav
|
|
177
|
+
if(shortVals.n === "a") { keyvals.nav = "any"; }
|
|
178
|
+
else if(shortVals.n === "s") { keyvals.nav = "seq"; }
|
|
179
|
+
if(shortVals.n === "n") { keyvals.nav = "none"; }
|
|
180
|
+
|
|
181
|
+
// Pic
|
|
182
|
+
if(shortVals.p !== "") { keyvals.pic = shortVals.p; }
|
|
183
|
+
|
|
184
|
+
// XYZ
|
|
185
|
+
if(shortVals.c !== "") { keyvals.xyz = shortVals.c; }
|
|
186
|
+
|
|
187
|
+
// Map
|
|
188
|
+
if(shortVals.m !== "") { keyvals.map = shortVals.m; }
|
|
189
|
+
|
|
190
|
+
// Date
|
|
191
|
+
if(shortVals.d !== "") { keyvals.date_from = shortVals.d; }
|
|
192
|
+
if(shortVals.e !== "") { keyvals.date_to = shortVals.e; }
|
|
193
|
+
|
|
194
|
+
// Pic type
|
|
195
|
+
if(shortVals.t === "f") { keyvals.pic_type = "flat"; }
|
|
196
|
+
else if(shortVals.t === "e") { keyvals.pic_type = "equirectangular"; }
|
|
197
|
+
|
|
198
|
+
// Camera
|
|
199
|
+
if(shortVals.k !== "") { keyvals.camera = shortVals.k; }
|
|
200
|
+
|
|
201
|
+
// Theme
|
|
202
|
+
if(shortVals.v === "d") { keyvals.theme = "default"; }
|
|
203
|
+
else if(shortVals.v === "a") { keyvals.theme = "age"; }
|
|
204
|
+
else if(shortVals.v === "t") { keyvals.theme = "type"; }
|
|
205
|
+
|
|
206
|
+
// Background
|
|
207
|
+
if(shortVals.b === "s") { keyvals.background = "streets"; }
|
|
208
|
+
else if(shortVals.b === "a") { keyvals.background = "aerial"; }
|
|
209
|
+
|
|
210
|
+
// Users
|
|
211
|
+
if(shortVals.u !== "") { keyvals.users = shortVals.u; }
|
|
212
|
+
}
|
|
147
213
|
|
|
148
214
|
return keyvals;
|
|
149
215
|
}
|
|
@@ -189,7 +255,7 @@ export default class URLHash extends EventTarget {
|
|
|
189
255
|
* @private
|
|
190
256
|
*/
|
|
191
257
|
_onHashChange() {
|
|
192
|
-
|
|
258
|
+
let vals = this._getCurrentHash();
|
|
193
259
|
|
|
194
260
|
// Restore selected picture
|
|
195
261
|
if(vals.pic) {
|
|
@@ -245,6 +311,36 @@ export default class URLHash extends EventTarget {
|
|
|
245
311
|
}
|
|
246
312
|
}
|
|
247
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Get short link URL (hash replaced by Base64)
|
|
316
|
+
* @returns {str} The short link URL
|
|
317
|
+
*/
|
|
318
|
+
getShortLink(baseUrl) {
|
|
319
|
+
const url = new URL(baseUrl);
|
|
320
|
+
const hashParts = this._getHashParts();
|
|
321
|
+
const shortVals = {
|
|
322
|
+
f: (hashParts.focus || "").substring(0, 1),
|
|
323
|
+
s: !isNaN(parseInt(hashParts.speed)) ? Math.floor(parseInt(hashParts.speed)/100) : undefined,
|
|
324
|
+
n: (hashParts.nav || "").substring(0, 1),
|
|
325
|
+
p: hashParts.pic,
|
|
326
|
+
c: hashParts.xyz,
|
|
327
|
+
m: hashParts.map,
|
|
328
|
+
d: hashParts.date_from,
|
|
329
|
+
e: hashParts.date_to,
|
|
330
|
+
t: (hashParts.pic_type || "").substring(0, 1),
|
|
331
|
+
k: hashParts.camera,
|
|
332
|
+
v: (hashParts.theme || "").substring(0, 1),
|
|
333
|
+
b: (hashParts.background || "").substring(0, 1),
|
|
334
|
+
u: hashParts.users,
|
|
335
|
+
};
|
|
336
|
+
const short = Object.entries(shortVals)
|
|
337
|
+
.filter(([,v]) => v != undefined && v != "")
|
|
338
|
+
.map(([k,v]) => `${k}${v}`)
|
|
339
|
+
.join("|");
|
|
340
|
+
url.hash = `s=${short}`;
|
|
341
|
+
return url;
|
|
342
|
+
}
|
|
343
|
+
|
|
248
344
|
/**
|
|
249
345
|
* Extracts from hash parsed keys all map filters values
|
|
250
346
|
* @param {*} vals Hash keys
|
package/src/viewer/Widgets.js
CHANGED
|
@@ -1172,7 +1172,7 @@ export default class Widgets {
|
|
|
1172
1172
|
const btnId = pnlShare.querySelector("#gvs-edit-id");
|
|
1173
1173
|
const btnRss = pnlShare.querySelector("#gvs-share-rss");
|
|
1174
1174
|
|
|
1175
|
-
fUrl.setAttribute("data-copy", baseUrl);
|
|
1175
|
+
fUrl.setAttribute("data-copy", this._viewer?._hash?.getShortLink(baseUrl) || baseUrl);
|
|
1176
1176
|
fIframe.innerText = `<iframe src="${iframeBaseUrl}" style="border: none; width: 500px; height: 300px"></iframe>`;
|
|
1177
1177
|
|
|
1178
1178
|
const meta = this._viewer.psv.getPictureMetadata();
|
|
@@ -1192,6 +1192,7 @@ export default class Widgets {
|
|
|
1192
1192
|
};
|
|
1193
1193
|
|
|
1194
1194
|
updateLinks();
|
|
1195
|
+
this._viewer.addEventListener("ready", updateLinks, { once: true });
|
|
1195
1196
|
this._viewer?._hash?.addEventListener("url-changed", updateLinks);
|
|
1196
1197
|
|
|
1197
1198
|
// Copy to clipboard on button click
|
|
@@ -26,6 +26,7 @@ jest.mock("maplibre-gl", () => ({
|
|
|
26
26
|
return {
|
|
27
27
|
layers: [],
|
|
28
28
|
sources: {},
|
|
29
|
+
metadata: {},
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
resize() {;}
|
|
@@ -52,7 +53,7 @@ const createParent = () => ({
|
|
|
52
53
|
getDataBbox: jest.fn(),
|
|
53
54
|
getPicturesTilesUrl: jest.fn(),
|
|
54
55
|
_getMapRequestTransform: jest.fn(),
|
|
55
|
-
getMapStyle: () => ({ sources: {}, layers: [] }),
|
|
56
|
+
getMapStyle: () => ({ sources: {}, layers: [], metadata: {} }),
|
|
56
57
|
},
|
|
57
58
|
_t: {
|
|
58
59
|
maplibre: {},
|
package/tests/utils/I18n.test.js
CHANGED
|
@@ -1,4 +1,87 @@
|
|
|
1
|
-
import { getTranslations } from "../../src/utils/I18n";
|
|
1
|
+
import { autoDetectLocale, getTranslations } from "../../src/utils/I18n";
|
|
2
|
+
|
|
3
|
+
describe("autoDetectLocale", () => {
|
|
4
|
+
// Mock the window.navigator.languages
|
|
5
|
+
const originalNavigatorLanguages = window.navigator.languages;
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
// Reset window.navigator.languages after each test
|
|
9
|
+
Object.defineProperty(window.navigator, "languages", {
|
|
10
|
+
value: originalNavigatorLanguages,
|
|
11
|
+
configurable: true
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns matched language from supportedTranslations", () => {
|
|
16
|
+
Object.defineProperty(window.navigator, "languages", {
|
|
17
|
+
value: ["fr-FR", "en-US"],
|
|
18
|
+
configurable: true
|
|
19
|
+
});
|
|
20
|
+
const supportedTranslations = ["en", "fr", "es"];
|
|
21
|
+
const fallback = "en";
|
|
22
|
+
expect(autoDetectLocale(supportedTranslations, fallback)).toBe("fr");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns fallback when no match is found", () => {
|
|
26
|
+
Object.defineProperty(window.navigator, "languages", {
|
|
27
|
+
value: ["de-DE", "it-IT"],
|
|
28
|
+
configurable: true
|
|
29
|
+
});
|
|
30
|
+
const supportedTranslations = ["en", "fr", "es"];
|
|
31
|
+
const fallback = "en";
|
|
32
|
+
expect(autoDetectLocale(supportedTranslations, fallback)).toBe("en");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns zh_Hant for Chinese Traditional locales", () => {
|
|
36
|
+
Object.defineProperty(window.navigator, "languages", {
|
|
37
|
+
value: ["zh-TW"],
|
|
38
|
+
configurable: true
|
|
39
|
+
});
|
|
40
|
+
const supportedTranslations = ["zh_Hant", "zh_Hans", "en"];
|
|
41
|
+
const fallback = "en";
|
|
42
|
+
expect(autoDetectLocale(supportedTranslations, fallback)).toBe("zh_Hant");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns zh_Hans for Chinese Simplified locales", () => {
|
|
46
|
+
Object.defineProperty(window.navigator, "languages", {
|
|
47
|
+
value: ["zh-CN"],
|
|
48
|
+
configurable: true
|
|
49
|
+
});
|
|
50
|
+
const supportedTranslations = ["zh_Hant", "zh_Hans", "en"];
|
|
51
|
+
const fallback = "en";
|
|
52
|
+
expect(autoDetectLocale(supportedTranslations, fallback)).toBe("zh_Hans");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns first matched language even when navigator language has region", () => {
|
|
56
|
+
Object.defineProperty(window.navigator, "languages", {
|
|
57
|
+
value: ["fr-CA", "en-US"],
|
|
58
|
+
configurable: true
|
|
59
|
+
});
|
|
60
|
+
const supportedTranslations = ["fr", "en"];
|
|
61
|
+
const fallback = "en";
|
|
62
|
+
expect(autoDetectLocale(supportedTranslations, fallback)).toBe("fr");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns fallback when supportedTranslations is empty", () => {
|
|
66
|
+
Object.defineProperty(window.navigator, "languages", {
|
|
67
|
+
value: ["fr-FR"],
|
|
68
|
+
configurable: true
|
|
69
|
+
});
|
|
70
|
+
const supportedTranslations = [];
|
|
71
|
+
const fallback = "en";
|
|
72
|
+
expect(autoDetectLocale(supportedTranslations, fallback)).toBe("en");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("handles language codes with more than two characters", () => {
|
|
76
|
+
Object.defineProperty(window.navigator, "languages", {
|
|
77
|
+
value: ["pt-BR", "en-US"],
|
|
78
|
+
configurable: true
|
|
79
|
+
});
|
|
80
|
+
const supportedTranslations = ["pt", "en"];
|
|
81
|
+
const fallback = "en";
|
|
82
|
+
expect(autoDetectLocale(supportedTranslations, fallback)).toBe("pt");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
2
85
|
|
|
3
86
|
describe("getTranslations", () => {
|
|
4
87
|
it("works with default lang", () => {
|