@panoramax/web-viewer 3.1.0 → 3.1.1-develop-5214442e
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 +19 -1
- package/build/index.css +7 -7
- package/build/index.css.map +1 -1
- package/build/index.js +15 -5
- package/build/index.js.map +1 -1
- package/docs/02_Usage.md +237 -223
- package/docs/05_Compatibility.md +14 -0
- package/package.json +8 -8
- package/src/components/Map.js +12 -5
- package/src/components/Photo.js +2 -1
- package/src/translations/en.json +2 -1
- package/src/translations/fr.json +2 -1
- package/src/translations/zh_Hant.json +15 -1
- package/src/utils/API.js +7 -3
- package/src/utils/I18n.js +13 -6
- package/src/utils/Map.js +42 -1
- package/src/utils/Utils.js +1 -1
- package/src/viewer/URLHash.js +102 -6
- package/src/viewer/Widgets.js +10 -2
- package/tests/Editor.test.js +1 -0
- package/tests/StandaloneMap.test.js +1 -0
- package/tests/Viewer.test.js +2 -0
- package/tests/components/CoreView.test.js +1 -0
- package/tests/components/Map.test.js +3 -1
- package/tests/utils/API.test.js +1 -0
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panoramax/web-viewer",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1-develop-5214442e",
|
|
4
4
|
"description": "Panoramax web viewer for geolocated pictures",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"author": "Panoramax team",
|
|
@@ -90,14 +90,14 @@
|
|
|
90
90
|
"dependencies": {
|
|
91
91
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
|
92
92
|
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
|
93
|
-
"@photo-sphere-viewer/core": "5.
|
|
94
|
-
"@photo-sphere-viewer/equirectangular-tiles-adapter": "5.
|
|
95
|
-
"@photo-sphere-viewer/gallery-plugin": "5.
|
|
96
|
-
"@photo-sphere-viewer/markers-plugin": "5.
|
|
97
|
-
"@photo-sphere-viewer/virtual-tour-plugin": "5.
|
|
93
|
+
"@photo-sphere-viewer/core": "5.11.0-beta.1",
|
|
94
|
+
"@photo-sphere-viewer/equirectangular-tiles-adapter": "5.11.0-beta.1",
|
|
95
|
+
"@photo-sphere-viewer/gallery-plugin": "5.11.0-beta.1",
|
|
96
|
+
"@photo-sphere-viewer/markers-plugin": "5.11.0-beta.1",
|
|
97
|
+
"@photo-sphere-viewer/virtual-tour-plugin": "5.11.0-beta.1",
|
|
98
98
|
"documentation": "^14.0.1",
|
|
99
|
-
"maplibre-gl": "^
|
|
100
|
-
"pmtiles": "^2.
|
|
99
|
+
"maplibre-gl": "^4.7.1",
|
|
100
|
+
"pmtiles": "^3.2.0"
|
|
101
101
|
},
|
|
102
102
|
"eslintConfig": {
|
|
103
103
|
"env": {
|
package/src/components/Map.js
CHANGED
|
@@ -96,6 +96,13 @@ export default class Map extends maplibregl.Map {
|
|
|
96
96
|
// Parent selection
|
|
97
97
|
this._parent.addEventListener("select", this.reloadLayersStyles.bind(this));
|
|
98
98
|
|
|
99
|
+
// Timeout for initial loading
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
if(!this.loaded() && this._parent._loader.isVisible()) {
|
|
102
|
+
this._parent._loader.dismiss({}, this._parent._t.map.slow_loading, () => {});
|
|
103
|
+
}
|
|
104
|
+
}, 15000);
|
|
105
|
+
|
|
99
106
|
this.on("load", async () => {
|
|
100
107
|
await this.setVisibleUsers(this._parent._options.users);
|
|
101
108
|
this.reloadLayersStyles();
|
|
@@ -291,10 +298,10 @@ export default class Map extends maplibregl.Map {
|
|
|
291
298
|
filterUserLayersContent(dataType, filter) {
|
|
292
299
|
[...this._userLayers].forEach(l => {
|
|
293
300
|
this.setFilter(getUserLayerId(l, dataType), filter);
|
|
294
|
-
if(dataType === "sequences") {
|
|
301
|
+
if(dataType === "sequences" && this.getLayer(getUserLayerId(l, "sequences_plus"))) {
|
|
295
302
|
this.setFilter(getUserLayerId(l, "sequences_plus"), filter);
|
|
296
303
|
}
|
|
297
|
-
if(dataType === "pictures") {
|
|
304
|
+
if(dataType === "pictures" && this.getLayer(getUserLayerId(l, "pictures_symbols"))) {
|
|
298
305
|
this.setFilter(getUserLayerId(l, "pictures_symbols"), filter);
|
|
299
306
|
}
|
|
300
307
|
});
|
|
@@ -362,7 +369,7 @@ export default class Map extends maplibregl.Map {
|
|
|
362
369
|
Object.entries(style.sources).forEach(([sId, s]) => this.addSource(sId, s));
|
|
363
370
|
style.layers = style.layers || [];
|
|
364
371
|
const layers = style.layers.concat(getMissingLayerStyles(style.sources, style.layers));
|
|
365
|
-
layers.forEach(l => this.addLayer(l, firstLabelLayerId?.id));
|
|
372
|
+
layers.filter(l => Object.keys(l).length > 0).forEach(l => this.addLayer(l, firstLabelLayerId?.id));
|
|
366
373
|
}
|
|
367
374
|
|
|
368
375
|
// Map interaction events
|
|
@@ -609,7 +616,7 @@ export default class Map extends maplibregl.Map {
|
|
|
609
616
|
if(thumbUrl) {
|
|
610
617
|
let content = document.createElement("img");
|
|
611
618
|
content.classList.add("gvs-map-thumb");
|
|
612
|
-
content.alt = this._parent._t.thumbnail;
|
|
619
|
+
content.alt = this._parent._t.map.thumbnail;
|
|
613
620
|
let img = new Image();
|
|
614
621
|
img.src = thumbUrl;
|
|
615
622
|
|
|
@@ -634,7 +641,7 @@ export default class Map extends maplibregl.Map {
|
|
|
634
641
|
}
|
|
635
642
|
}
|
|
636
643
|
else {
|
|
637
|
-
this._picPopup.setHTML(`<i>${this._parent._t.no_thumbnail}</i>`);
|
|
644
|
+
this._picPopup.setHTML(`<i>${this._parent._t.map.no_thumbnail}</i>`);
|
|
638
645
|
}
|
|
639
646
|
}
|
|
640
647
|
};
|
package/src/components/Photo.js
CHANGED
|
@@ -194,7 +194,8 @@ export default class Photo extends PSViewer {
|
|
|
194
194
|
rotation: following && sameSequence && animated,
|
|
195
195
|
rotateTo: this.getPosition()
|
|
196
196
|
};
|
|
197
|
-
|
|
197
|
+
// Constant direction related to North
|
|
198
|
+
// nodeTransition.rotateTo.yaw += fromNodeHeading - toNodeHeading;
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
}
|
package/src/translations/en.json
CHANGED
|
@@ -161,6 +161,7 @@
|
|
|
161
161
|
"loading": "Loading…",
|
|
162
162
|
"thumbnail": "Thumbnail of hovered picture",
|
|
163
163
|
"no_thumbnail": "No thumbnail",
|
|
164
|
-
"not_public": "Not publicly visible"
|
|
164
|
+
"not_public": "Not publicly visible",
|
|
165
|
+
"slow_loading": "Map is slow to load and could appear broken"
|
|
165
166
|
}
|
|
166
167
|
}
|
package/src/translations/fr.json
CHANGED
|
@@ -161,6 +161,7 @@
|
|
|
161
161
|
"loading": "Chargement…",
|
|
162
162
|
"thumbnail": "Miniature de la photo survolée",
|
|
163
163
|
"no_thumbnail": "Pas de miniature",
|
|
164
|
-
"not_public": "Masqué au public"
|
|
164
|
+
"not_public": "Masqué au public",
|
|
165
|
+
"slow_loading": "La carte est longue à charger et pourrait apparaître cassée"
|
|
165
166
|
}
|
|
166
167
|
}
|
|
@@ -120,7 +120,17 @@
|
|
|
120
120
|
"show_psv": "顯示相片檢視器",
|
|
121
121
|
"zoom": "縮放",
|
|
122
122
|
"zoomIn": "拉近",
|
|
123
|
-
"zoomOut": "拉遠"
|
|
123
|
+
"zoomOut": "拉遠",
|
|
124
|
+
"report_submit": "送出",
|
|
125
|
+
"report_email": "您的 email",
|
|
126
|
+
"report_nature_label": "問題的類型",
|
|
127
|
+
"report_nature": {
|
|
128
|
+
"": "選擇問題的類型…"
|
|
129
|
+
},
|
|
130
|
+
"share_embed_docs": "深入了解嵌入設定值",
|
|
131
|
+
"error_retry": "請稍後再試",
|
|
132
|
+
"error_click": "點擊以繼續",
|
|
133
|
+
"legend_title": "顯示相片的詳細資料"
|
|
124
134
|
},
|
|
125
135
|
"map": {
|
|
126
136
|
"loading": "正在載入…",
|
|
@@ -132,5 +142,9 @@
|
|
|
132
142
|
"ctrlZoom": "使用 CTRL + 滑鼠滾輪縮放圖片",
|
|
133
143
|
"loadError": "無法載入全景圖",
|
|
134
144
|
"twoFingers": "使用雙指移動"
|
|
145
|
+
},
|
|
146
|
+
"maplibre": {
|
|
147
|
+
"GeolocateControl.FindMyLocation": "尋找我的位置",
|
|
148
|
+
"GeolocateControl.LocationNotAvailable": "無法使用位置資訊"
|
|
135
149
|
}
|
|
136
150
|
}
|
package/src/utils/API.js
CHANGED
|
@@ -285,7 +285,9 @@ export default class API {
|
|
|
285
285
|
* @returns {object} The fetch options
|
|
286
286
|
*/
|
|
287
287
|
_getFetchOptions() {
|
|
288
|
-
return Object.assign({
|
|
288
|
+
return Object.assign({
|
|
289
|
+
signal: AbortSignal.timeout(15000)
|
|
290
|
+
}, this._fetchOpts);
|
|
289
291
|
}
|
|
290
292
|
|
|
291
293
|
/**
|
|
@@ -295,8 +297,10 @@ export default class API {
|
|
|
295
297
|
* @returns {function} The RequestTransformFunction
|
|
296
298
|
*/
|
|
297
299
|
_getMapRequestTransform() {
|
|
300
|
+
const fetchOpts = this._getFetchOptions();
|
|
301
|
+
delete fetchOpts.signal;
|
|
298
302
|
// Only if tiles endpoint is enabled and fetch options set
|
|
299
|
-
if(Object.keys(
|
|
303
|
+
if(Object.keys(fetchOpts).length > 0) {
|
|
300
304
|
return (url) => {
|
|
301
305
|
// As MapLibre will use this function for all its calls
|
|
302
306
|
// We must make sure fetch options are sent only for
|
|
@@ -304,7 +308,7 @@ export default class API {
|
|
|
304
308
|
if(url.startsWith(this._endpoint)) {
|
|
305
309
|
return {
|
|
306
310
|
url,
|
|
307
|
-
...
|
|
311
|
+
...fetchOpts
|
|
308
312
|
};
|
|
309
313
|
}
|
|
310
314
|
};
|
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/utils/Utils.js
CHANGED
|
@@ -338,7 +338,7 @@ export function apiFeatureToPSVNode(f, t, fastInternet=false, customLinkFilter=n
|
|
|
338
338
|
let panorama;
|
|
339
339
|
|
|
340
340
|
// Cropped panorama
|
|
341
|
-
if(Object.keys(croppedPanoData).length > 0) {
|
|
341
|
+
if(!tileUrl && Object.keys(croppedPanoData).length > 0) {
|
|
342
342
|
panorama = {
|
|
343
343
|
baseUrl: fastInternet ? hdUrl : baseUrl,
|
|
344
344
|
origBaseUrl: fastInternet ? hdUrl : baseUrl,
|
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
|
@@ -723,6 +723,8 @@ export default class Widgets {
|
|
|
723
723
|
const overridenGeocoder = query => {
|
|
724
724
|
const rgxCoords = /([-+]?\d{1,2}\.\d+),\s*([-+]?\d{1,3}\.\d+)/;
|
|
725
725
|
const coordsMatch = query.match(rgxCoords);
|
|
726
|
+
const rgxUuid = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/;
|
|
727
|
+
const uuidMatch = query.match(rgxUuid);
|
|
726
728
|
|
|
727
729
|
if(coordsMatch) {
|
|
728
730
|
const lat = parseFloat(coordsMatch[1]);
|
|
@@ -732,7 +734,12 @@ export default class Widgets {
|
|
|
732
734
|
zoom: 16,
|
|
733
735
|
});
|
|
734
736
|
return Promise.resolve(true);
|
|
735
|
-
}
|
|
737
|
+
}
|
|
738
|
+
else if(uuidMatch) {
|
|
739
|
+
this._viewer.select(null, query);
|
|
740
|
+
return Promise.resolve(true);
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
736
743
|
return this._viewer.map.geocoder({
|
|
737
744
|
query,
|
|
738
745
|
limit: 3,
|
|
@@ -1172,7 +1179,7 @@ export default class Widgets {
|
|
|
1172
1179
|
const btnId = pnlShare.querySelector("#gvs-edit-id");
|
|
1173
1180
|
const btnRss = pnlShare.querySelector("#gvs-share-rss");
|
|
1174
1181
|
|
|
1175
|
-
fUrl.setAttribute("data-copy", baseUrl);
|
|
1182
|
+
fUrl.setAttribute("data-copy", this._viewer?._hash?.getShortLink(baseUrl) || baseUrl);
|
|
1176
1183
|
fIframe.innerText = `<iframe src="${iframeBaseUrl}" style="border: none; width: 500px; height: 300px"></iframe>`;
|
|
1177
1184
|
|
|
1178
1185
|
const meta = this._viewer.psv.getPictureMetadata();
|
|
@@ -1192,6 +1199,7 @@ export default class Widgets {
|
|
|
1192
1199
|
};
|
|
1193
1200
|
|
|
1194
1201
|
updateLinks();
|
|
1202
|
+
this._viewer.addEventListener("ready", updateLinks, { once: true });
|
|
1195
1203
|
this._viewer?._hash?.addEventListener("url-changed", updateLinks);
|
|
1196
1204
|
|
|
1197
1205
|
// Copy to clipboard on button click
|
package/tests/Editor.test.js
CHANGED
package/tests/Viewer.test.js
CHANGED
|
@@ -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: {},
|
|
@@ -205,6 +206,7 @@ describe("filterUserLayersContent", () => {
|
|
|
205
206
|
const m = new Map(p, c);
|
|
206
207
|
m.getSource = () => true;
|
|
207
208
|
m.setPaintProperty = jest.fn();
|
|
209
|
+
m.getLayer = () => true;
|
|
208
210
|
m._fire("load");
|
|
209
211
|
m.setFilter = jest.fn();
|
|
210
212
|
m.filterUserLayersContent("pictures", [["test", "true"]]);
|
package/tests/utils/API.test.js
CHANGED
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", () => {
|