@panoramax/web-viewer 3.0.2-develop-a8ea8e60

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.
Files changed (125) hide show
  1. package/.dockerignore +6 -0
  2. package/.gitlab-ci.yml +71 -0
  3. package/CHANGELOG.md +428 -0
  4. package/CODE_OF_CONDUCT.md +134 -0
  5. package/Dockerfile +14 -0
  6. package/LICENSE +21 -0
  7. package/README.md +39 -0
  8. package/build/editor.html +1 -0
  9. package/build/index.css +36 -0
  10. package/build/index.css.map +1 -0
  11. package/build/index.html +1 -0
  12. package/build/index.js +25 -0
  13. package/build/index.js.map +1 -0
  14. package/build/map.html +1 -0
  15. package/build/viewer.html +1 -0
  16. package/config/env.js +104 -0
  17. package/config/getHttpsConfig.js +66 -0
  18. package/config/getPackageJson.js +25 -0
  19. package/config/jest/babelTransform.js +29 -0
  20. package/config/jest/cssTransform.js +14 -0
  21. package/config/jest/fileTransform.js +40 -0
  22. package/config/modules.js +134 -0
  23. package/config/paths.js +72 -0
  24. package/config/pnpTs.js +35 -0
  25. package/config/webpack/persistentCache/createEnvironmentHash.js +9 -0
  26. package/config/webpack.config.js +885 -0
  27. package/config/webpackDevServer.config.js +127 -0
  28. package/docs/01_Start.md +149 -0
  29. package/docs/02_Usage.md +828 -0
  30. package/docs/03_URL_settings.md +140 -0
  31. package/docs/04_Advanced_examples.md +214 -0
  32. package/docs/05_Compatibility.md +85 -0
  33. package/docs/09_Develop.md +62 -0
  34. package/docs/90_Releases.md +27 -0
  35. package/docs/images/class_diagram.drawio +129 -0
  36. package/docs/images/class_diagram.jpg +0 -0
  37. package/docs/images/screenshot.jpg +0 -0
  38. package/mkdocs.yml +45 -0
  39. package/package.json +254 -0
  40. package/public/editor.html +54 -0
  41. package/public/favicon.ico +0 -0
  42. package/public/index.html +59 -0
  43. package/public/map.html +53 -0
  44. package/public/viewer.html +67 -0
  45. package/scripts/build.js +217 -0
  46. package/scripts/start.js +176 -0
  47. package/scripts/test.js +52 -0
  48. package/src/Editor.css +37 -0
  49. package/src/Editor.js +359 -0
  50. package/src/StandaloneMap.js +114 -0
  51. package/src/Viewer.css +203 -0
  52. package/src/Viewer.js +1186 -0
  53. package/src/components/CoreView.css +64 -0
  54. package/src/components/CoreView.js +159 -0
  55. package/src/components/Loader.css +56 -0
  56. package/src/components/Loader.js +111 -0
  57. package/src/components/Map.css +65 -0
  58. package/src/components/Map.js +841 -0
  59. package/src/components/Photo.css +36 -0
  60. package/src/components/Photo.js +687 -0
  61. package/src/img/arrow_360.svg +14 -0
  62. package/src/img/arrow_flat.svg +11 -0
  63. package/src/img/arrow_triangle.svg +10 -0
  64. package/src/img/arrow_turn.svg +9 -0
  65. package/src/img/bg_aerial.jpg +0 -0
  66. package/src/img/bg_streets.jpg +0 -0
  67. package/src/img/loader_base.jpg +0 -0
  68. package/src/img/loader_hd.jpg +0 -0
  69. package/src/img/logo_dead.svg +91 -0
  70. package/src/img/marker.svg +17 -0
  71. package/src/img/marker_blue.svg +20 -0
  72. package/src/img/switch_big.svg +44 -0
  73. package/src/img/switch_mini.svg +48 -0
  74. package/src/index.js +10 -0
  75. package/src/translations/de.json +163 -0
  76. package/src/translations/en.json +164 -0
  77. package/src/translations/eo.json +6 -0
  78. package/src/translations/es.json +164 -0
  79. package/src/translations/fi.json +1 -0
  80. package/src/translations/fr.json +164 -0
  81. package/src/translations/hu.json +133 -0
  82. package/src/translations/nl.json +1 -0
  83. package/src/translations/zh_Hant.json +136 -0
  84. package/src/utils/API.js +709 -0
  85. package/src/utils/Exif.js +198 -0
  86. package/src/utils/I18n.js +75 -0
  87. package/src/utils/Map.js +382 -0
  88. package/src/utils/PhotoAdapter.js +45 -0
  89. package/src/utils/Utils.js +568 -0
  90. package/src/utils/Widgets.js +477 -0
  91. package/src/viewer/URLHash.js +334 -0
  92. package/src/viewer/Widgets.css +711 -0
  93. package/src/viewer/Widgets.js +1196 -0
  94. package/tests/Editor.test.js +125 -0
  95. package/tests/StandaloneMap.test.js +44 -0
  96. package/tests/Viewer.test.js +363 -0
  97. package/tests/__snapshots__/Editor.test.js.snap +300 -0
  98. package/tests/__snapshots__/StandaloneMap.test.js.snap +30 -0
  99. package/tests/__snapshots__/Viewer.test.js.snap +195 -0
  100. package/tests/components/CoreView.test.js +91 -0
  101. package/tests/components/Loader.test.js +38 -0
  102. package/tests/components/Map.test.js +230 -0
  103. package/tests/components/Photo.test.js +335 -0
  104. package/tests/components/__snapshots__/Loader.test.js.snap +15 -0
  105. package/tests/components/__snapshots__/Map.test.js.snap +767 -0
  106. package/tests/components/__snapshots__/Photo.test.js.snap +205 -0
  107. package/tests/data/Map_geocoder_ban.json +36 -0
  108. package/tests/data/Map_geocoder_nominatim.json +56 -0
  109. package/tests/data/Viewer_pictures_1.json +148 -0
  110. package/tests/setupTests.js +5 -0
  111. package/tests/utils/API.test.js +906 -0
  112. package/tests/utils/Exif.test.js +124 -0
  113. package/tests/utils/I18n.test.js +28 -0
  114. package/tests/utils/Map.test.js +105 -0
  115. package/tests/utils/Utils.test.js +300 -0
  116. package/tests/utils/Widgets.test.js +107 -0
  117. package/tests/utils/__snapshots__/API.test.js.snap +132 -0
  118. package/tests/utils/__snapshots__/Exif.test.js.snap +43 -0
  119. package/tests/utils/__snapshots__/Map.test.js.snap +48 -0
  120. package/tests/utils/__snapshots__/Utils.test.js.snap +41 -0
  121. package/tests/utils/__snapshots__/Widgets.test.js.snap +44 -0
  122. package/tests/viewer/URLHash.test.js +537 -0
  123. package/tests/viewer/Widgets.test.js +127 -0
  124. package/tests/viewer/__snapshots__/URLHash.test.js.snap +98 -0
  125. package/tests/viewer/__snapshots__/Widgets.test.js.snap +393 -0
@@ -0,0 +1,537 @@
1
+ import URLHash from "../../src/viewer/URLHash";
2
+
3
+ describe("constructor", () => {
4
+ it("works", () => {
5
+ const receivedEvents = [];
6
+ const vl = (t, h) => {
7
+ receivedEvents.push([t, h]);
8
+ if(t == "ready") { h(); }
9
+ };
10
+ const v = { addEventListener: vl };
11
+ const uh = new URLHash(v);
12
+ expect(uh).toBeDefined();
13
+ expect(uh._viewer).toBe(v);
14
+ expect(receivedEvents).toMatchSnapshot();
15
+ });
16
+ });
17
+
18
+ describe("destroy", () => {
19
+ it("works", () => {
20
+ const v = {
21
+ addEventListener: jest.fn(),
22
+ removeEventListener: jest.fn(),
23
+ };
24
+ const uh = new URLHash(v);
25
+ uh.destroy();
26
+ expect(uh._viewer).toBeUndefined();
27
+ expect(v.removeEventListener.mock.calls).toMatchSnapshot();
28
+ });
29
+ });
30
+
31
+ describe("bindMapEvents", () => {
32
+ it("works", () => {
33
+ const v = { addEventListener: jest.fn(), map: { on: jest.fn() } };
34
+ const uh = new URLHash(v);
35
+ uh.bindMapEvents();
36
+ expect(v.map.on.mock.calls).toMatchSnapshot();
37
+ });
38
+ });
39
+
40
+ describe("getHashString", () => {
41
+ it("works without any specific values set", () => {
42
+ const v = {
43
+ addEventListener: jest.fn(),
44
+ getPicturesNavigation: () => null,
45
+ psv: {
46
+ getTransitionDuration: () => null,
47
+ getPictureMetadata: () => null,
48
+ },
49
+ };
50
+ const uh = new URLHash(v);
51
+ expect(uh.getHashString()).toBe("#map=none");
52
+ });
53
+
54
+ it("works with picture metadata", () => {
55
+ const v = {
56
+ addEventListener: jest.fn(),
57
+ getPicturesNavigation: () => null,
58
+ psv: {
59
+ getTransitionDuration: () => null,
60
+ getPictureMetadata: () => ({ "id": "cbfc3add-8173-4464-98c8-de2a43c6a50f" })
61
+ },
62
+ };
63
+ const uh = new URLHash(v);
64
+ uh._getXyzHashString = () => "0/1/2";
65
+ expect(uh.getHashString()).toBe("#map=none&pic=cbfc3add-8173-4464-98c8-de2a43c6a50f&xyz=0/1/2");
66
+ });
67
+
68
+ it("works with map started + wide", () => {
69
+ const v = {
70
+ addEventListener: jest.fn(),
71
+ getPicturesNavigation: () => null,
72
+ psv: {
73
+ getTransitionDuration: () => null,
74
+ getPictureMetadata: () => null,
75
+ },
76
+ map: {
77
+ getVisibleUsers: () => ["geovisio"],
78
+ hasTwoBackgrounds: () => false,
79
+ },
80
+ isMapWide: () => true,
81
+ popupContainer: { classList: { contains: () => true } },
82
+ };
83
+ const uh = new URLHash(v);
84
+ uh._getMapHashString = () => "18/0.5/-12";
85
+ expect(uh.getHashString()).toBe("#focus=map&map=18/0.5/-12");
86
+ });
87
+
88
+ it("works with map + picture wide", () => {
89
+ const v = {
90
+ addEventListener: jest.fn(),
91
+ getPicturesNavigation: () => null,
92
+ psv: {
93
+ getTransitionDuration: () => null,
94
+ getPictureMetadata: () => ({ "id": "cbfc3add-8173-4464-98c8-de2a43c6a50f" }),
95
+ },
96
+ map: {
97
+ getVisibleUsers: () => ["geovisio"],
98
+ hasTwoBackgrounds: () => false,
99
+ },
100
+ isMapWide: () => false,
101
+ popupContainer: { classList: { contains: () => true } },
102
+ };
103
+ const uh = new URLHash(v);
104
+ uh._getXyzHashString = () => "0/1/2";
105
+ uh._getMapHashString = () => "18/0.5/-12";
106
+ expect(uh.getHashString()).toBe("#focus=pic&map=18/0.5/-12&pic=cbfc3add-8173-4464-98c8-de2a43c6a50f&xyz=0/1/2");
107
+ });
108
+
109
+ it("works with map filters", () => {
110
+ const v = {
111
+ addEventListener: jest.fn(),
112
+ getPicturesNavigation: () => null,
113
+ psv: {
114
+ getTransitionDuration: () => null,
115
+ getPictureMetadata: () => null,
116
+ },
117
+ map: {
118
+ getVisibleUsers: () => ["geovisio"],
119
+ hasTwoBackgrounds: () => false,
120
+ },
121
+ isMapWide: () => false,
122
+ popupContainer: { classList: { contains: () => true } },
123
+ _mapFilters: {
124
+ "minDate": "2023-01-01",
125
+ "maxDate": "2023-08-08",
126
+ "camera": "sony",
127
+ "type": "flat",
128
+ "theme": "age",
129
+ },
130
+ };
131
+ const uh = new URLHash(v);
132
+ uh._getMapHashString = () => "18/0.5/-12";
133
+ expect(uh.getHashString()).toBe("#camera=sony&date_from=2023-01-01&date_to=2023-08-08&focus=pic&map=18/0.5/-12&pic_type=flat&theme=age");
134
+ });
135
+
136
+ it("works with speed", () => {
137
+ const v = {
138
+ addEventListener: jest.fn(),
139
+ getPicturesNavigation: () => null,
140
+ psv: {
141
+ getTransitionDuration: () => 250,
142
+ getPictureMetadata: () => null,
143
+ },
144
+ };
145
+ const uh = new URLHash(v);
146
+ expect(uh.getHashString()).toBe("#map=none&speed=250");
147
+ });
148
+
149
+ it("works with popup", () => {
150
+ const v = {
151
+ addEventListener: jest.fn(),
152
+ getPicturesNavigation: () => null,
153
+ psv: {
154
+ getTransitionDuration: () => null,
155
+ getPictureMetadata: () => null,
156
+ },
157
+ map: {
158
+ getVisibleUsers: () => ["geovisio"],
159
+ hasTwoBackgrounds: () => false,
160
+ },
161
+ isMapWide: () => false,
162
+ popupContainer: { classList: { contains: () => false } },
163
+ };
164
+ const uh = new URLHash(v);
165
+ uh._getMapHashString = () => "18/0.5/-12";
166
+ expect(uh.getHashString()).toBe("#focus=meta&map=18/0.5/-12");
167
+ });
168
+
169
+ it("works with nav", () => {
170
+ const v = {
171
+ addEventListener: jest.fn(),
172
+ getPicturesNavigation: () => "pic",
173
+ psv: {
174
+ getTransitionDuration: () => null,
175
+ getPictureMetadata: () => null,
176
+ },
177
+ };
178
+ const uh = new URLHash(v);
179
+ expect(uh.getHashString()).toBe("#map=none&nav=pic");
180
+ });
181
+ });
182
+
183
+ describe("_getCurrentHash", () => {
184
+ it("works if empty", () => {
185
+ delete window.location;
186
+ window.location = { hash: "" };
187
+ const v = { addEventListener: jest.fn() };
188
+ const uh = new URLHash(v);
189
+ expect(uh._getCurrentHash()).toStrictEqual({});
190
+ });
191
+
192
+ it("works with single param", () => {
193
+ delete window.location;
194
+ window.location = { hash: "#a=b" };
195
+ const v = { addEventListener: jest.fn() };
196
+ const uh = new URLHash(v);
197
+ expect(uh._getCurrentHash()).toStrictEqual({"a": "b"});
198
+ });
199
+
200
+ it("works with multiple params", () => {
201
+ delete window.location;
202
+ window.location = { hash: "#a=b&c=d" };
203
+ const v = { addEventListener: jest.fn() };
204
+ const uh = new URLHash(v);
205
+ expect(uh._getCurrentHash()).toStrictEqual({"a": "b", "c": "d"});
206
+ });
207
+ });
208
+
209
+ describe("_getMapHashString", () => {
210
+ it("works with zoom+center", () => {
211
+ const v = {
212
+ addEventListener: jest.fn(),
213
+ map: {
214
+ getZoom: () => 18,
215
+ getCenter: () => ({ lng: -12.5, lat: 48.75 }),
216
+ getBearing: () => null,
217
+ getPitch: () => null,
218
+ }
219
+ };
220
+ const uh = new URLHash(v);
221
+ expect(uh._getMapHashString()).toBe("18/48.75/-12.5");
222
+ });
223
+
224
+ it("works with zoom+center+bearing", () => {
225
+ const v = {
226
+ addEventListener: jest.fn(),
227
+ map: {
228
+ getZoom: () => 18,
229
+ getCenter: () => ({ lng: -12.5, lat: 48.75 }),
230
+ getBearing: () => 12,
231
+ getPitch: () => null,
232
+ }
233
+ };
234
+ const uh = new URLHash(v);
235
+ expect(uh._getMapHashString()).toBe("18/48.75/-12.5/12");
236
+ });
237
+
238
+ it("works with zoom+center+pitch", () => {
239
+ const v = {
240
+ addEventListener: jest.fn(),
241
+ map: {
242
+ getZoom: () => 18,
243
+ getCenter: () => ({ lng: -12.5, lat: 48.75 }),
244
+ getBearing: () => null,
245
+ getPitch: () => 65,
246
+ },
247
+ };
248
+ const uh = new URLHash(v);
249
+ expect(uh._getMapHashString()).toBe("18/48.75/-12.5/0/65");
250
+ });
251
+
252
+ it("works with zoom+center+bearing+pitch", () => {
253
+ const v = {
254
+ addEventListener: jest.fn(),
255
+ map: {
256
+ getZoom: () => 18,
257
+ getCenter: () => ({ lng: -12.5, lat: 48.75 }),
258
+ getBearing: () => 42,
259
+ getPitch: () => 65,
260
+ },
261
+ };
262
+ const uh = new URLHash(v);
263
+ expect(uh._getMapHashString()).toBe("18/48.75/-12.5/42/65");
264
+ });
265
+ });
266
+
267
+ describe("_getXyzHashString", () => {
268
+ it("works", () => {
269
+ const v = {
270
+ addEventListener: jest.fn(),
271
+ psv: {
272
+ getXYZ: () => ({ x: 12, y: 50, z: 75 }),
273
+ },
274
+ };
275
+ const uh = new URLHash(v);
276
+ expect(uh._getXyzHashString()).toBe("12.00/50.00/75");
277
+ });
278
+
279
+ it("rounds to 2 decimals", () => {
280
+ const v = {
281
+ addEventListener: jest.fn(),
282
+ psv: {
283
+ getXYZ: () => ({ x: 12.123456, y: 50.789456, z: 75 }),
284
+ },
285
+ };
286
+ const uh = new URLHash(v);
287
+ expect(uh._getXyzHashString()).toBe("12.12/50.79/75");
288
+ });
289
+
290
+ it("works without z", () => {
291
+ const v = {
292
+ addEventListener: jest.fn(),
293
+ psv: {
294
+ getXYZ: () => ({ x: 12, y: 50 }),
295
+ },
296
+ };
297
+ const uh = new URLHash(v);
298
+ expect(uh._getXyzHashString()).toBe("12.00/50.00/0");
299
+ });
300
+ });
301
+
302
+ describe("_onHashChange", () => {
303
+ global.console = { warn: jest.fn() };
304
+
305
+ it("works", () => {
306
+ const v = {
307
+ addEventListener: jest.fn(),
308
+ map: {
309
+ dragRotate: { isEnabled: () => false },
310
+ getBearing: jest.fn(),
311
+ jumpTo: jest.fn(),
312
+ setVisibleUsers: jest.fn(),
313
+ setBackground: jest.fn(),
314
+ },
315
+ psv: {
316
+ setTransitionDuration: jest.fn(),
317
+ setXYZ: jest.fn(),
318
+ },
319
+ select: jest.fn(),
320
+ setFocus: jest.fn(),
321
+ setFilters: jest.fn(),
322
+ };
323
+ const uh = new URLHash(v);
324
+ uh._getCurrentHash = () => ({
325
+ pic: "cbfc3add-8173-4464-98c8-de2a43c6a50f",
326
+ focus: "map",
327
+ xyz: "1/2/3",
328
+ map: "15/48.7/-12.5",
329
+ speed: "300",
330
+ });
331
+ uh._onHashChange();
332
+
333
+ expect(v.select.mock.calls).toEqual([[null, "cbfc3add-8173-4464-98c8-de2a43c6a50f"]]);
334
+ expect(v.setFocus.mock.calls).toEqual([["map"]]);
335
+ expect(v.map.jumpTo.mock.calls).toEqual([[{ center: [-12.5, 48.7], zoom: 15, pitch: 0 }]]);
336
+ expect(v.psv.setXYZ.mock.calls).toEqual([[1, 2, 3]]);
337
+ expect(v.psv.setTransitionDuration.mock.calls).toEqual([["300"]]);
338
+ });
339
+
340
+ it("doesnt call map if no map params", () => {
341
+ const v = {
342
+ addEventListener: jest.fn(),
343
+ map: {
344
+ jumpTo: jest.fn(),
345
+ setBackground: jest.fn(),
346
+ },
347
+ psv: {
348
+ setTransitionDuration: jest.fn(),
349
+ setXYZ: jest.fn(),
350
+ },
351
+ select: jest.fn(),
352
+ setFocus: jest.fn(),
353
+ setFilters: jest.fn(),
354
+ };
355
+ const uh = new URLHash(v);
356
+ uh._getCurrentHash = () => ({
357
+ pic: "cbfc3add-8173-4464-98c8-de2a43c6a50f",
358
+ xyz: "1/2/3",
359
+ });
360
+ uh._onHashChange();
361
+
362
+ expect(v.select.mock.calls).toEqual([[null, "cbfc3add-8173-4464-98c8-de2a43c6a50f"]]);
363
+ expect(v.setFocus.mock.calls.length).toBe(0);
364
+ expect(v.map.jumpTo.mock.calls.length).toBe(0);
365
+ expect(v.map.setBackground.mock.calls.length).toBe(0);
366
+ expect(v.psv.setXYZ.mock.calls).toEqual([[1, 2, 3]]);
367
+ });
368
+
369
+ it("doesnt call psv if no related params", () => {
370
+ const v = {
371
+ addEventListener: jest.fn(),
372
+ map: {
373
+ dragRotate: { isEnabled: () => false },
374
+ getBearing: jest.fn(),
375
+ jumpTo: jest.fn(),
376
+ setVisibleUsers: jest.fn(),
377
+ setBackground: jest.fn(),
378
+ },
379
+ psv: {
380
+ setTransitionDuration: jest.fn(),
381
+ setXYZ: jest.fn(),
382
+ },
383
+ select: jest.fn(),
384
+ setFocus: jest.fn(),
385
+ setFilters: jest.fn(),
386
+ };
387
+ const uh = new URLHash(v);
388
+ uh._getCurrentHash = () => ({
389
+ focus: "map",
390
+ map: "15/48.7/-12.5"
391
+ });
392
+ uh._onHashChange();
393
+
394
+ expect(v.select.mock.calls.length).toEqual(0);
395
+ expect(v.setFocus.mock.calls).toEqual([["map"]]);
396
+ expect(v.map.jumpTo.mock.calls).toEqual([[{ center: [-12.5, 48.7], zoom: 15, pitch: 0 }]]);
397
+ expect(v.psv.setXYZ.mock.calls.length).toEqual(0);
398
+ });
399
+
400
+ it("handles multiple picture IDs", () => {
401
+ const v = {
402
+ addEventListener: jest.fn(),
403
+ map: {
404
+ dragRotate: { isEnabled: () => false },
405
+ getBearing: jest.fn(),
406
+ jumpTo: jest.fn(),
407
+ setVisibleUsers: jest.fn(),
408
+ setBackground: jest.fn(),
409
+ },
410
+ psv: {
411
+ setTransitionDuration: jest.fn(),
412
+ setXYZ: jest.fn(),
413
+ },
414
+ select: jest.fn(),
415
+ setFocus: jest.fn(),
416
+ setFilters: jest.fn(),
417
+ };
418
+ const uh = new URLHash(v);
419
+ uh._getCurrentHash = () => ({
420
+ pic: "cbfc3add-8173-4464-98c8-de2a43c6a50f;blablabla-8173-4464-98c8-de2a43c6a50f",
421
+ focus: "map",
422
+ xyz: "1/2/3",
423
+ map: "15/48.7/-12.5",
424
+ speed: "300",
425
+ });
426
+ uh._onHashChange();
427
+
428
+ expect(v.select.mock.calls).toEqual([[null, "cbfc3add-8173-4464-98c8-de2a43c6a50f"]]);
429
+ expect(v.setFocus.mock.calls).toEqual([["map"]]);
430
+ expect(v.map.jumpTo.mock.calls).toEqual([[{ center: [-12.5, 48.7], zoom: 15, pitch: 0 }]]);
431
+ expect(v.psv.setXYZ.mock.calls).toEqual([[1, 2, 3]]);
432
+ expect(v.psv.setTransitionDuration.mock.calls).toEqual([["300"]]);
433
+ });
434
+ });
435
+
436
+ describe("getMapFiltersFromHashVals", () => {
437
+ it("works", () => {
438
+ const v = { addEventListener: jest.fn() };
439
+ const uh = new URLHash(v);
440
+ const vals = {
441
+ "date_from": "2023-01-01",
442
+ "date_to": "2023-05-05",
443
+ "pic_type": "equirectangular",
444
+ "camera": "sony",
445
+ "whatever": "whenever",
446
+ "theme": "type",
447
+ };
448
+ expect(uh.getMapFiltersFromHashVals(vals)).toEqual({
449
+ "minDate": "2023-01-01",
450
+ "maxDate": "2023-05-05",
451
+ "type": "equirectangular",
452
+ "camera": "sony",
453
+ "theme": "type",
454
+ });
455
+ });
456
+ });
457
+
458
+ describe("getMapOptionsFromHashString", () => {
459
+ it("works without map", () => {
460
+ const v = { addEventListener: jest.fn() };
461
+ const uh = new URLHash(v);
462
+ expect(uh.getMapOptionsFromHashString("18/-12.5/48.7")).toEqual({ center: [48.7, -12.5], zoom: 18, pitch: 0 });
463
+ });
464
+
465
+ it("works with map", () => {
466
+ const v = {
467
+ addEventListener: jest.fn(),
468
+ map: {
469
+ dragRotate: { isEnabled: () => true },
470
+ touchZoomRotate: { isEnabled: () => true },
471
+ },
472
+ };
473
+ const uh = new URLHash(v);
474
+ expect(uh.getMapOptionsFromHashString("18/-12.5/48.7/15/12")).toEqual({ center: [48.7, -12.5], zoom: 18, pitch: 12, bearing: 15 });
475
+ });
476
+
477
+ it("nulls if string is invalid", () => {
478
+ const v = { addEventListener: jest.fn() };
479
+ const uh = new URLHash(v);
480
+ expect(uh.getMapOptionsFromHashString("bla/bla/bla")).toBeNull();
481
+ });
482
+ });
483
+
484
+ describe("getXyzOptionsFromHashString", () => {
485
+ it("works", () => {
486
+ const v = { addEventListener: jest.fn() };
487
+ const uh = new URLHash(v);
488
+ expect(uh.getXyzOptionsFromHashString("18/-12.5/48.7")).toEqual({ x: 18, y: -12.5, z: 48.7 });
489
+ });
490
+
491
+ it("nulls if string is invalid", () => {
492
+ const v = { addEventListener: jest.fn() };
493
+ const uh = new URLHash(v);
494
+ expect(uh.getXyzOptionsFromHashString("bla/bla/bla")).toBeNull();
495
+ });
496
+ });
497
+
498
+ describe("_updateHash", () => {
499
+ it("works", async () => {
500
+ delete window.history;
501
+ delete window.location;
502
+
503
+ window.history = { replaceState: jest.fn(), state: {} };
504
+ window.location = { href: "http://localhost:5000/#a=b&b=c" };
505
+
506
+ const v = { addEventListener: jest.fn() };
507
+ const uh = new URLHash(v);
508
+ uh.getHashString = () => "#c=d";
509
+ uh.dispatchEvent = jest.fn();
510
+
511
+ uh._updateHash();
512
+ await new Promise((r) => setTimeout(r, 1000));
513
+
514
+ expect(window.history.replaceState.mock.calls.pop()).toEqual([{}, null, "http://localhost:5000/#c=d"]);
515
+ expect(uh.dispatchEvent.mock.calls).toMatchSnapshot();
516
+ });
517
+
518
+ it("deduplicates calls", async () => {
519
+ delete window.history;
520
+ delete window.location;
521
+
522
+ window.history = { replaceState: jest.fn(), state: {} };
523
+ window.location = { href: "http://localhost:5000/#a=b&b=c" };
524
+
525
+ const v = { addEventListener: jest.fn() };
526
+ const uh = new URLHash(v);
527
+ uh.getHashString = () => "#c=d";
528
+
529
+ for(let i=0; i <= 10; i++) {
530
+ uh._updateHash();
531
+ }
532
+
533
+ await new Promise((r) => setTimeout(r, 1000));
534
+
535
+ expect(window.history.replaceState.mock.calls).toEqual([[{}, null, "http://localhost:5000/#c=d"]]);
536
+ });
537
+ });
@@ -0,0 +1,127 @@
1
+ import Widgets from "../../src/viewer/Widgets.js";
2
+ import T_fr from "../../src/translations/fr.json";
3
+
4
+ const TRANSLATIONS = {"fr": T_fr};
5
+
6
+ jest.mock("@photo-sphere-viewer/core", () => ({
7
+ Viewer: class {},
8
+ SYSTEM: {},
9
+ DEFAULTS: {},
10
+ }));
11
+
12
+ jest.mock("@photo-sphere-viewer/equirectangular-tiles-adapter", () => ({
13
+ EquirectangularTilesAdapter: jest.fn(),
14
+ }));
15
+
16
+ jest.mock("@photo-sphere-viewer/virtual-tour-plugin", () => ({
17
+ VirtualTourPlugin: jest.fn(),
18
+ }));
19
+
20
+ jest.mock("maplibre-gl", () => ({
21
+ addProtocol: jest.fn(),
22
+ Map: class {},
23
+ }));
24
+
25
+ const createViewer = () => {
26
+ const vc = document.createElement("div");
27
+ const vmac = document.createElement("div");
28
+ const vmin = document.createElement("div");
29
+ vc.appendChild(vmac);
30
+ vc.appendChild(vmin);
31
+ return {
32
+ _t: TRANSLATIONS.fr,
33
+ _api: {
34
+ _endpoints: {},
35
+ getRSSURL: jest.fn(),
36
+ },
37
+ container: vc,
38
+ mainContainer: vmac,
39
+ miniContainer: vmin,
40
+ isWidthSmall: () => false,
41
+ addEventListener: jest.fn(),
42
+ dispatchEvent: jest.fn(),
43
+ setPopup: jest.fn(),
44
+ getPicturesNavigation: () => null,
45
+ psv: {
46
+ getTransitionDuration: jest.fn(),
47
+ getPictureMetadata: jest.fn(),
48
+ getZoomLevel: jest.fn(),
49
+ getPictureOriginalHeading: jest.fn(),
50
+ },
51
+ };
52
+ };
53
+
54
+ describe("constructor", () => {
55
+ it("works", () => {
56
+ const v = createViewer();
57
+ const w = new Widgets(v, {bla: "bla"});
58
+ expect(v.container.innerHTML).toMatchSnapshot();
59
+ expect(w._options).toStrictEqual({bla: "bla", editIdUrl: "https://www.openstreetmap.org/edit"});
60
+ });
61
+
62
+ it("works with small container", () => {
63
+ const v = createViewer();
64
+ v.isWidthSmall = () => true;
65
+ const w = new Widgets(v);
66
+ expect(v.container.innerHTML).toMatchSnapshot();
67
+ });
68
+
69
+ it("works with iframeBaseURL option", () => {
70
+ const v = createViewer();
71
+ const w = new Widgets(v, { iframeBaseURL: "https://geovisio.fr/iframed/" });
72
+ expect(w._options.iframeBaseURL).toEqual("https://geovisio.fr/iframed/");
73
+ expect(v.container.innerHTML).toMatchSnapshot();
74
+ });
75
+ });
76
+
77
+ describe("_showPictureMetadataPopup", () => {
78
+ it("fails if no picture is selected", () =>{
79
+ const v = createViewer();
80
+ v.psv.getPictureMetadata = () => null;
81
+ const w = new Widgets(v);
82
+ expect(() => w._showPictureMetadataPopup()).toThrowError("No picture currently selected");
83
+ });
84
+
85
+ it("works when metadata is available", () => {
86
+ const v = createViewer();
87
+ v._api.getPictureMetadataUrl = () => "https://geovisio.fr/api/picture/metadata.json";
88
+ v._api.getSequenceMetadataUrl = () => "https://geovisio.fr/api/sequence/metadata.json";
89
+ v.psv.getPictureMetadata = () => ({
90
+ id: "blablabla",
91
+ caption: { producer: "Adrien PAVIE", license: "CC-BY-SA 4.0", date: new Date("2024-01-01") },
92
+ panorama: {
93
+ baseUrl: "https://geovisio.fr/api/pictures/blablabla/sd.jpg",
94
+ hdUrl: "https://geovisio.fr/api/pictures/blablabla/hd.jpg",
95
+ cols: 2,
96
+ rows: 1,
97
+ width: 2048,
98
+ tileUrl: () => "https://geovisio.fr/api/pictures/blablabla/tile.jpg"
99
+ },
100
+ links: {},
101
+ gps: [-1.7, 48.6],
102
+ sequence: {
103
+ id: "seq",
104
+ nextPic: "blanext",
105
+ prevPic: "blaprev"
106
+ },
107
+ sphereCorrection: { pan: 0 },
108
+ horizontalFov: 90,
109
+ properties: {
110
+ "view:azimuth": 90,
111
+ "pers:interior_orientation": {
112
+ camera_manufacturer: "IKEA",
113
+ camera_model: "360 en Kit",
114
+ focal_length: 3
115
+ },
116
+ exif: {
117
+ "Exif.GPSInfo.GPSDOP": 1
118
+ }
119
+ }
120
+ });
121
+ const w = new Widgets(v);
122
+ Date.prototype.toLocaleDateString = () => "1 janvier 2024 à 01:00:00,000";
123
+ w._showPictureMetadataPopup();
124
+ expect(v.setPopup.mock.calls).toMatchSnapshot();
125
+ expect(v.dispatchEvent.mock.calls).toMatchSnapshot();
126
+ });
127
+ });