@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,687 @@
1
+ import "./Photo.css";
2
+ import LoaderImgBase from "../img/loader_base.jpg";
3
+ import LogoDead from "../img/logo_dead.svg";
4
+ import {
5
+ getDistance, positionToXYZ, xyzToPosition,
6
+ apiFeatureToPSVNode, getRelativeHeading,
7
+ } from "../utils/Utils";
8
+
9
+ // Photo Sphere Viewer imports
10
+ import "@photo-sphere-viewer/core/index.css";
11
+ import "@photo-sphere-viewer/virtual-tour-plugin/index.css";
12
+ import "@photo-sphere-viewer/gallery-plugin/index.css";
13
+ import "@photo-sphere-viewer/markers-plugin/index.css";
14
+ import { Viewer as PSViewer } from "@photo-sphere-viewer/core";
15
+ import { VirtualTourPlugin } from "@photo-sphere-viewer/virtual-tour-plugin";
16
+ import PhotoAdapter from "../utils/PhotoAdapter";
17
+
18
+
19
+ // Default panorama (logo)
20
+ const BASE_PANORAMA = {
21
+ baseUrl: LoaderImgBase,
22
+ width: 1280,
23
+ cols: 2,
24
+ rows: 1,
25
+ tileUrl: () => null,
26
+ };
27
+
28
+ export const PSV_DEFAULT_ZOOM = 30;
29
+ export const PSV_ANIM_DURATION = 250;
30
+ export const PIC_MAX_STAY_DURATION = 3000;
31
+
32
+ PSViewer.useNewAnglesOrder = true;
33
+
34
+ /**
35
+ * Photo is the component showing a single picture.
36
+ * It uses Photo Sphere Viewer as a basis, and pre-configure dialog with STAC API.
37
+ *
38
+ * Note that all functions of [PhotoSphereViewer Viewer class](https://photo-sphere-viewer.js.org/api/classes/core.viewer) are available as well.
39
+ *
40
+ * @param {CoreView} parent The parent view
41
+ * @param {Element} container The DOM element to create into
42
+ * @param {object} [options] The viewer options. Can be any of [Photo Sphere Viewer options](https://photo-sphere-viewer.js.org/guide/config.html#standard-options)
43
+ * @param {number} [options.transitionDuration] The number of milliseconds the transition animation should be.
44
+ * @param {function} [options.shouldGoFast] Function returning a boolean to indicate if we may skip loading HD images.
45
+ */
46
+ export default class Photo extends PSViewer {
47
+ constructor(parent, container, options = {}) {
48
+ super({
49
+ container,
50
+ adapter: [PhotoAdapter, {
51
+ showErrorTile: false,
52
+ baseBlur: false,
53
+ resolution: parent.isWidthSmall() ? 32 : 64,
54
+ shouldGoFast: options.shouldGoFast,
55
+ }],
56
+ withCredentials: parent._options?.fetchOptions?.credentials == "include",
57
+ requestHeaders: parent._options?.fetchOptions?.headers,
58
+ panorama: BASE_PANORAMA,
59
+ lang: parent._t.psv,
60
+ minFov: 5,
61
+ loadingTxt: " ",
62
+ navbar: null,
63
+ rendererParameters: {
64
+ preserveDrawingBuffer: !parent.isWidthSmall(),
65
+ },
66
+ plugins: [
67
+ [VirtualTourPlugin, {
68
+ dataMode: "server",
69
+ positionMode: "gps",
70
+ renderMode: "3d",
71
+ preload: true,
72
+ getNode: () => {},
73
+ transitionOptions: () => {},
74
+ arrowsPosition: {
75
+ linkOverlapAngle: Math.PI / 6,
76
+ }
77
+ }],
78
+ ],
79
+ ...options
80
+ });
81
+
82
+ this._parent = parent;
83
+ container.classList.add("gvs-psv");
84
+ this._shouldGoFast = options?.shouldGoFast || (() => false);
85
+ this._transitionDuration = options?.transitionDuration || PSV_ANIM_DURATION;
86
+ this._myVTour = this.getPlugin(VirtualTourPlugin);
87
+ this._myVTour.datasource.nodeResolver = this._getNodeFromAPI.bind(this);
88
+ this._myVTour.config.transitionOptions = this._psvNodeTransition.bind(this);
89
+ this._clearArrows = this._myVTour.arrowsRenderer.clear.bind(this._myVTour.arrowsRenderer);
90
+ this._myVTour.arrowsRenderer.clear = () => {};
91
+
92
+ // Cache to find sequence ID for a single picture
93
+ this._picturesSequences = {};
94
+
95
+ // Offer various custom events
96
+ this._myVTour.addEventListener("enter-arrow", this._onEnterArrow.bind(this));
97
+ this._myVTour.addEventListener("leave-arrow", this._onLeaveArrow.bind(this));
98
+ this._myVTour.addEventListener("node-changed", this._onNodeChanged.bind(this));
99
+ this.addEventListener("position-updated", this._onPositionUpdated.bind(this));
100
+ this.addEventListener("zoom-updated", this._onZoomUpdated.bind(this));
101
+ this._parent.addEventListener("select", this._onSelect.bind(this));
102
+
103
+ // Fix for loader circle background not showing up
104
+ this.loader.size = 150;
105
+ this.loader.color = "rgba(61, 61, 61, 0.5)";
106
+ this.loader.textColor = "rgba(255, 255, 255, 0.7)";
107
+ this.loader.border = 5;
108
+ this.loader.thickness = 10;
109
+ this.loader.canvas.setAttribute("viewBox", "0 0 150 150");
110
+ this.loader.__updateContent();
111
+ }
112
+
113
+ /**
114
+ * Calls API to retrieve a certain picture, then transforms into PSV format
115
+ *
116
+ * @private
117
+ * @param {string} picId The picture UUID
118
+ * @returns {Promise} Resolves on PSV node metadata
119
+ */
120
+ async _getNodeFromAPI(picId) {
121
+ if(!picId) { return; }
122
+
123
+ const picApiResponse = await fetch(
124
+ this._parent._api.getPictureMetadataUrl(picId, this._picturesSequences[picId]),
125
+ this._parent._api._getFetchOptions()
126
+ );
127
+ let metadata = await picApiResponse.json();
128
+
129
+ if(metadata.features) { metadata = metadata.features.pop(); }
130
+ if(!metadata || Object.keys(metadata).length === 0 || !picApiResponse.ok) {
131
+ if(this._parent._loader) {
132
+ this._parent._loader.dismiss(true, this._parent._t.gvs.error_pic);
133
+ }
134
+ throw new Error("Picture with ID " + picId + " was not found");
135
+ }
136
+
137
+ this._picturesSequences[picId] = metadata.collection;
138
+ const node = apiFeatureToPSVNode(
139
+ metadata,
140
+ this._parent._t,
141
+ this._parent._isInternetFast,
142
+ this._parent._picturesNavFilter?.bind(this._parent)
143
+ );
144
+ if(node?.sequence?.prevPic) { this._picturesSequences[node?.sequence?.prevPic] = metadata.collection; }
145
+ if(node?.sequence?.nextPic) { this._picturesSequences[node?.sequence?.nextPic] = metadata.collection; }
146
+
147
+ return node;
148
+ }
149
+
150
+ /**
151
+ * PSV node transition handler
152
+ * @param {*} toNode Next loading node
153
+ * @param {*} [fromNode] Currently shown node (previous)
154
+ * @param {*} [fromLink] Link clicked by user to go from current to next node
155
+ * @private
156
+ */
157
+ _psvNodeTransition(toNode, fromNode, fromLink) {
158
+ let nodeTransition = {};
159
+
160
+ const animationDuration = this._shouldGoFast() ? 0 : Math.min(PSV_ANIM_DURATION, this._transitionDuration);
161
+ const animated = animationDuration > 100;
162
+ const following = (fromLink || fromNode?.links.find(a => a.nodeId == toNode.id)) != null;
163
+ const sameSequence = fromNode && toNode.sequence.id === fromNode.sequence.id;
164
+ const fromNodeHeading = (fromNode?.properties?.["view:azimuth"] || 0) * (Math.PI / 180);
165
+ const toNodeHeading = (toNode?.properties?.["view:azimuth"] || 0) * (Math.PI / 180);
166
+
167
+ this.setOption("maxFov", Math.min(toNode.horizontalFov * 3/4, 90));
168
+
169
+ const centerNoAnim = {
170
+ speed: 0,
171
+ fadeIn: false,
172
+ rotation: false,
173
+ rotateTo: { pitch: 0, yaw: 0 },
174
+ zoomTo: PSV_DEFAULT_ZOOM
175
+ };
176
+
177
+ // Going to 360
178
+ if(toNode.horizontalFov == 360) {
179
+ // No previous sequence -> Point to center + no animation
180
+ if(!fromNode) {
181
+ nodeTransition = centerNoAnim;
182
+ }
183
+ // Has a previous sequence
184
+ else {
185
+ // Far away sequences -> Point to center + no animation
186
+ if(getDistance(fromNode.gps, toNode.gps) >= 0.001) {
187
+ nodeTransition = centerNoAnim;
188
+ }
189
+ // Nearby sequences -> Keep orientation
190
+ else {
191
+ nodeTransition = {
192
+ speed: animationDuration,
193
+ fadeIn: following && animated,
194
+ rotation: following && sameSequence && animated,
195
+ rotateTo: this.getPosition()
196
+ };
197
+ nodeTransition.rotateTo.yaw += fromNodeHeading - toNodeHeading;
198
+ }
199
+ }
200
+ }
201
+ // Going to flat
202
+ else {
203
+ // Same sequence -> Point to center + animation if following pics + not vomiting
204
+ if(sameSequence) {
205
+ const fromYaw = this.getPosition().yaw;
206
+ const fovMaxYaw = (fromNode.horizontalFov * (Math.PI / 180)) / 2;
207
+ const keepZoomPos = fromYaw <= fovMaxYaw || fromYaw >= (2 * Math.PI - fovMaxYaw);
208
+ const notTooMuchRotation = Math.abs(fromNodeHeading - toNodeHeading) <= Math.PI / 4;
209
+ nodeTransition = {
210
+ speed: animationDuration,
211
+ fadeIn: following && notTooMuchRotation && animated,
212
+ rotation: following && notTooMuchRotation && animated,
213
+ rotateTo: keepZoomPos ? this.getPosition() : { pitch: 0, yaw: 0 },
214
+ zoomTo: keepZoomPos ? this.getZoomLevel() : PSV_DEFAULT_ZOOM,
215
+ };
216
+ }
217
+ // Different sequence -> Point to center + no animation
218
+ else {
219
+ nodeTransition = centerNoAnim;
220
+ }
221
+ }
222
+
223
+ if(nodeTransition.fadeIn && nodeTransition.speed >= 150) {
224
+ setTimeout(this._clearArrows, nodeTransition.speed-100);
225
+ }
226
+ else {
227
+ this._clearArrows();
228
+ }
229
+
230
+
231
+ /**
232
+ * Event for picture starting to load
233
+ *
234
+ * @event psv:picture-loading
235
+ * @memberof CoreView
236
+ * @type {object}
237
+ * @property {object} detail Event information
238
+ * @property {string} detail.picId The picture unique identifier
239
+ * @property {number} detail.lon Longitude (WGS84)
240
+ * @property {number} detail.lat Latitude (WGS84)
241
+ * @property {number} detail.x New x position (in degrees, 0-360), corresponds to heading (0° = North, 90° = East, 180° = South, 270° = West)
242
+ * @property {number} detail.y New y position (in degrees)
243
+ * @property {number} detail.z New z position (0-100)
244
+ */
245
+ const event = new CustomEvent("psv:picture-loading", {
246
+ detail: {
247
+ ...Object.assign({},
248
+ this.getXYZ(),
249
+ nodeTransition.rotateTo ? { x: (toNodeHeading + nodeTransition.rotateTo.yaw) * 180 / Math.PI } : null,
250
+ nodeTransition.zoomTo ? { z: nodeTransition.zoomTo } : null
251
+ ),
252
+ picId: toNode.id,
253
+ lon: toNode.gps[0],
254
+ lat: toNode.gps[1]
255
+ }
256
+ });
257
+ this._parent.dispatchEvent(event);
258
+
259
+ return nodeTransition;
260
+ }
261
+
262
+ /**
263
+ * Event handler for PSV arrow hover.
264
+ * It creates a custom event "picture-preview-started"
265
+ * @private
266
+ * @param {object} e The event data
267
+ */
268
+ _onEnterArrow(e) {
269
+ const fromLink = e.link;
270
+ const fromNode = e.node;
271
+
272
+ // Find probable direction for previewed picture
273
+ let direction;
274
+ if(fromNode) {
275
+ if(fromNode.horizontalFov === 360) {
276
+ direction = (this.getPictureOriginalHeading() + this.getPosition().yaw * 180 / Math.PI) % 360;
277
+ }
278
+ else {
279
+ direction = this.getPictureOriginalHeading();
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Event for picture preview
285
+ *
286
+ * @event psv:picture-preview-started
287
+ * @memberof CoreView
288
+ * @type {object}
289
+ * @property {object} detail Event information
290
+ * @property {string} detail.picId The picture ID
291
+ * @property {number[]} detail.coordinates [x,y] coordinates
292
+ * @property {number} detail.direction The theorical picture orientation
293
+ */
294
+ const event = new CustomEvent("psv:picture-preview-started", { detail: {
295
+ picId: fromLink.nodeId,
296
+ coordinates: fromLink.gps,
297
+ direction,
298
+ }});
299
+ this._parent.dispatchEvent(event);
300
+ }
301
+
302
+ /**
303
+ * Event handler for PSV arrow end of hovering.
304
+ * It creates a custom event "picture-preview-stopped"
305
+ * @private
306
+ * @param {object} e The event data
307
+ */
308
+ _onLeaveArrow(e) {
309
+ const fromLink = e.link;
310
+
311
+ /**
312
+ * Event for end of picture preview
313
+ *
314
+ * @event psv:picture-preview-stopped
315
+ * @memberof CoreView
316
+ * @type {object}
317
+ * @property {object} detail Event information
318
+ * @property {string} detail.picId The picture ID
319
+ */
320
+ const event = new CustomEvent("psv:picture-preview-stopped", { detail: {
321
+ picId: fromLink.nodeId,
322
+ }});
323
+ this._parent.dispatchEvent(event);
324
+ }
325
+
326
+ /**
327
+ * Event handler for position update in PSV.
328
+ * Allows to send a custom "view-rotated" event.
329
+ * @private
330
+ */
331
+ _onPositionUpdated({position}) {
332
+ const pos = positionToXYZ(position, this.getZoomLevel());
333
+ pos.x += this.getPictureOriginalHeading();
334
+ pos.x = pos.x % 360;
335
+ /**
336
+ * Event for viewer rotation
337
+ *
338
+ * @event psv:view-rotated
339
+ * @memberof CoreView
340
+ * @type {object}
341
+ * @property {object} detail Event information
342
+ * @property {number} detail.x New x position (in degrees, 0-360), corresponds to heading (0° = North, 90° = East, 180° = South, 270° = West)
343
+ * @property {number} detail.y New y position (in degrees)
344
+ * @property {number} detail.z New Z position (between 0 and 100)
345
+ */
346
+ const event = new CustomEvent("psv:view-rotated", { detail: pos });
347
+ this._parent.dispatchEvent(event);
348
+
349
+ this._onTilesStartLoading();
350
+ }
351
+
352
+ /**
353
+ * Event handler for zoom updates in PSV.
354
+ * Allows to send a custom "view-rotated" event.
355
+ * @private
356
+ */
357
+ _onZoomUpdated({zoomLevel}) {
358
+ const event = new CustomEvent("psv:view-rotated", { detail: { ...this.getXY(), z: zoomLevel} });
359
+ this._parent.dispatchEvent(event);
360
+
361
+ this._onTilesStartLoading();
362
+ }
363
+
364
+ /**
365
+ * Event handler for node change in PSV.
366
+ * Allows to send a custom "picture-loaded" event.
367
+ * @private
368
+ */
369
+ _onNodeChanged(e) {
370
+ // Clean up clicked arrows
371
+ for(let d of document.getElementsByClassName("gvs-psv-tour-arrows")) {
372
+ d.classList.remove("gvs-clicked");
373
+ }
374
+
375
+ if(e.node.id) {
376
+ this._parent.select(e.node?.sequence?.id, e.node.id);
377
+ const picMeta = this.getPictureMetadata();
378
+ if(!picMeta) { return; }
379
+ this._prevSequence = picMeta.sequence.id;
380
+
381
+ /**
382
+ * Event for picture load (low-resolution image is loaded)
383
+ *
384
+ * @event psv:picture-loaded
385
+ * @memberof CoreView
386
+ * @type {object}
387
+ * @property {object} detail Event information
388
+ * @property {string} detail.picId The picture unique identifier
389
+ * @property {number} detail.lon Longitude (WGS84)
390
+ * @property {number} detail.lat Latitude (WGS84)
391
+ * @property {number} detail.x New x position (in degrees, 0-360), corresponds to heading (0° = North, 90° = East, 180° = South, 270° = West)
392
+ * @property {number} detail.y New y position (in degrees)
393
+ * @property {number} detail.z New z position (0-100)
394
+ */
395
+ const event = new CustomEvent("psv:picture-loaded", {
396
+ detail: {
397
+ ...this.getXYZ(),
398
+ picId: e.node.id,
399
+ lon: picMeta.gps[0],
400
+ lat: picMeta.gps[1]
401
+ }
402
+ });
403
+ this._parent.dispatchEvent(event);
404
+
405
+ // Change download URL
406
+ if(picMeta.panorama.hdUrl) {
407
+ this.setOption("downloadUrl", picMeta.panorama.hdUrl);
408
+ this.setOption("downloadName", e.node.id+".jpg");
409
+ }
410
+ else {
411
+ this.setOption("downloadUrl", null);
412
+ }
413
+ }
414
+
415
+ this._onTilesStartLoading();
416
+ }
417
+
418
+ /**
419
+ * Event handler for loading a new range of tiles
420
+ *
421
+ * @private
422
+ */
423
+ _onTilesStartLoading() {
424
+ if(this._tilesQueueTimer) {
425
+ clearInterval(this._tilesQueueTimer);
426
+ delete this._tilesQueueTimer;
427
+ }
428
+ this._tilesQueueTimer = setInterval(() => {
429
+ if(Object.keys(this.adapter.queue.tasks).length === 0) {
430
+ if(this._myVTour.state.currentNode) {
431
+ /**
432
+ * Event launched when all visible tiles of a picture are loaded
433
+ *
434
+ * @event psv:picture-tiles-loaded
435
+ * @memberof CoreView
436
+ * @type {object}
437
+ * @property {object} detail Event information
438
+ * @property {string} detail.picId The picture unique identifier
439
+ */
440
+ const event = new Event("psv:picture-tiles-loaded", { picId: this._myVTour.state.currentNode.id });
441
+ this._parent.dispatchEvent(event);
442
+ }
443
+ clearInterval(this._tilesQueueTimer);
444
+ delete this._tilesQueueTimer;
445
+ }
446
+ }, 100);
447
+ }
448
+
449
+ /**
450
+ * Access currently shown picture metadata
451
+ *
452
+ * @returns {object} Picture metadata
453
+ */
454
+ getPictureMetadata() {
455
+ return this._myVTour.state.currentNode ? Object.assign({}, this._myVTour.state.currentNode) : null;
456
+ }
457
+
458
+ /**
459
+ * Handler for select event.
460
+ * @private
461
+ */
462
+ _onSelect(e) {
463
+ if(e.detail.seqId) {
464
+ this._picturesSequences[e.detail.picId] = e.detail.seqId;
465
+ }
466
+
467
+ if(this._myVTour.getCurrentNode()?.id !== e.detail.picId) {
468
+ this.loader.show();
469
+ this._myVTour.setCurrentNode(e.detail.picId).catch(e => {
470
+ this.showErrorOverlay(e, this._parent._t.gvs.error_pic, true);
471
+ });
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Displays next picture in current sequence (if any)
477
+ */
478
+ goToNextPicture() {
479
+ if(!this.getPictureMetadata()) {
480
+ throw new Error("No picture currently selected");
481
+ }
482
+
483
+ const next = this.getPictureMetadata().sequence.nextPic;
484
+ if(next) {
485
+ this._parent.select(this.getPictureMetadata().sequence.id, next);
486
+ }
487
+ else {
488
+ throw new Error("No next picture available");
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Displays previous picture in current sequence (if any)
494
+ */
495
+ goToPrevPicture() {
496
+ if(!this.getPictureMetadata()) {
497
+ throw new Error("No picture currently selected");
498
+ }
499
+
500
+ const prev = this.getPictureMetadata().sequence.prevPic;
501
+ if(prev) {
502
+ this._parent.select(this.getPictureMetadata().sequence.id, prev);
503
+ }
504
+ else {
505
+ throw new Error("No previous picture available");
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Displays in viewer a picture near to given coordinates
511
+ *
512
+ * @param {number} lat Latitude (WGS84)
513
+ * @param {number} lon Longitude (WGS84)
514
+ * @returns {Promise} Resolves on picture ID if picture found, otherwise rejects
515
+ */
516
+ async goToPosition(lat, lon) {
517
+ return this._parent._api.getPicturesAroundCoordinates(lat, lon)
518
+ .then(res => {
519
+ if(res.features.length > 0) {
520
+ const f = res.features.pop();
521
+ this._parent.select(
522
+ f?.collection,
523
+ f.id
524
+ );
525
+ return f.id;
526
+ }
527
+ else {
528
+ return Promise.reject(new Error("No picture found nearby given coordinates"));
529
+ }
530
+ });
531
+ }
532
+
533
+ /**
534
+ * Get 2D position of sphere currently shown to user
535
+ *
536
+ * @returns {object} Position in format { x: heading in degrees (0° = North, 90° = East, 180° = South, 270° = West), y: top/bottom position in degrees (-90° = bottom, 0° = front, 90° = top) }
537
+ */
538
+ getXY() {
539
+ const pos = positionToXYZ(this.getPosition());
540
+ pos.x = (pos.x + this.getPictureOriginalHeading()) % 360;
541
+ return pos;
542
+ }
543
+
544
+ /**
545
+ * Get 3D position of sphere currently shown to user
546
+ *
547
+ * @returns {object} Position in format { x: heading in degrees (0° = North, 90° = East, 180° = South, 270° = West), y: top/bottom position in degrees (-90° = bottom, 0° = front, 90° = top), z: zoom (0 = wide, 100 = zoomed in) }
548
+ */
549
+ getXYZ() {
550
+ const pos = this.getXY();
551
+ pos.z = this.getZoomLevel();
552
+ return pos;
553
+ }
554
+
555
+ /**
556
+ * Get capture orientation of current picture, based on its GPS.
557
+ * @returns Picture original heading in degrees (0 to 360°)
558
+ */
559
+ getPictureOriginalHeading() {
560
+ return this.getPictureMetadata()?.properties?.["view:azimuth"] || 0;
561
+ }
562
+
563
+ /**
564
+ * Computes the relative heading of currently selected picture.
565
+ * This gives the angle of capture compared to sequence path (vehicle movement).
566
+ *
567
+ * @returns Relative heading in degrees (-180 to 180)
568
+ */
569
+ getPictureRelativeHeading() {
570
+ return getRelativeHeading(this.getPictureMetadata());
571
+ }
572
+
573
+ /**
574
+ * Clears the Photo Sphere Viewer metadata cache.
575
+ * It is useful when current picture or sequence has changed server-side after first load.
576
+ */
577
+ clearPictureMetadataCache() {
578
+ const oldPicId = this.getPictureMetadata()?.id;
579
+ const oldSeqId = this.getPictureMetadata()?.sequence?.id;
580
+
581
+ // Force deletion of cached metadata in PSV
582
+ this._myVTour.state.currentTooltip?.hide();
583
+ this._myVTour.state.currentTooltip = null;
584
+ this._myVTour.state.currentNode = null;
585
+ this._myVTour.state.preload = {};
586
+ this._myVTour.datasource.nodes = {};
587
+
588
+ // Reload current picture if one was selected
589
+ if(oldPicId) {
590
+ this._parent.select(oldSeqId, oldPicId);
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Change the shown position in picture
596
+ *
597
+ * @param {number} x X position (in degrees)
598
+ * @param {number} y Y position (in degrees)
599
+ * @param {number} z Z position (0-100)
600
+ */
601
+ setXYZ(x, y, z) {
602
+ const coords = xyzToPosition(x - this.getPictureOriginalHeading(), y, z);
603
+ this.rotate({ yaw: coords.yaw, pitch: coords.pitch });
604
+ this.zoom(coords.zoom);
605
+ }
606
+
607
+ /**
608
+ * Enable or disable higher contrast on picture
609
+ * @param {boolean} enable True to enable higher contrast
610
+ */
611
+ setHigherContrast(enable) {
612
+ this.renderer.renderer.toneMapping = enable ? 3 : 0;
613
+ this.renderer.renderer.toneMappingExposure = enable ? 2 : 1;
614
+ this.needsUpdate();
615
+ }
616
+
617
+ /**
618
+ * Get the duration of stay on a picture during a sequence play.
619
+ * @returns {number} The duration (in milliseconds)
620
+ */
621
+ getTransitionDuration() {
622
+ return this._transitionDuration;
623
+ }
624
+
625
+ /**
626
+ * Changes the duration of stay on a picture during a sequence play.
627
+ *
628
+ * @param {number} value The new duration (in milliseconds, between 100 and 3000)
629
+ */
630
+ setTransitionDuration(value) {
631
+ value = parseFloat(value);
632
+ if(value < 100 || value > PIC_MAX_STAY_DURATION) {
633
+ throw new Error("Invalid transition duration (should be between 100 and "+PIC_MAX_STAY_DURATION+")");
634
+ }
635
+ this._transitionDuration = value;
636
+
637
+ /**
638
+ * Event for transition duration change
639
+ *
640
+ * @event psv:transition-duration-changed
641
+ * @memberof CoreView
642
+ * @type {object}
643
+ * @property {object} detail Event information
644
+ * @property {string} detail.duration New duration (in milliseconds)
645
+ */
646
+ const event = new CustomEvent("psv:transition-duration-changed", { detail: { value } });
647
+ this._parent.dispatchEvent(event);
648
+ }
649
+
650
+ setPanorama(path, options) {
651
+ const onFailure = e => this.showErrorOverlay(e, this._parent._t.gvs.error_pic, true);
652
+ try {
653
+ return super.setPanorama(path, options).catch(onFailure);
654
+ }
655
+ catch(e) {
656
+ onFailure(e);
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Display an error message to user on screen
662
+ * @param {object} e The initial error
663
+ * @param {str} label The main error label to display
664
+ * @param {boolean} dissmisable Is error dissmisable
665
+ */
666
+ showErrorOverlay(e, label, dissmisable) {
667
+ if(this._parent._loader.isVisible() || !this.overlay.isVisible()) {
668
+ this._parent._loader.dismiss(
669
+ e,
670
+ label,
671
+ dissmisable ? () => {
672
+ this._parent._loader.dismiss();
673
+ this.overlay.hide();
674
+ } : undefined
675
+ );
676
+ }
677
+ else {
678
+ console.error(e);
679
+ this.overlay.show({
680
+ image: `<img style="width: 200px" src="${LogoDead}" />`,
681
+ title: this._parent._t.gvs.error,
682
+ text: label + "<br />" + this._parent._t.gvs.error_click,
683
+ dissmisable,
684
+ });
685
+ }
686
+ }
687
+ }
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ width="48"
4
+ height="48"
5
+ viewBox="0 0 48 48"
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ xmlns:svg="http://www.w3.org/2000/svg">
8
+ <path
9
+ style="fill:#ffcc80;fill-opacity:1;stroke:#ffffff;stroke-width:2.63736;stroke-dasharray:none;stroke-opacity:1"
10
+ d="M 24 1.3183594 A 22.68132 22.68132 0 0 0 1.3183594 24 A 22.68132 22.68132 0 0 0 24 46.681641 A 22.68132 22.68132 0 0 0 46.681641 24 A 22.68132 22.68132 0 0 0 24 1.3183594 z M 24 14.021484 A 9.9777565 9.9777565 0 0 1 33.978516 24 A 9.9777565 9.9777565 0 0 1 24 33.978516 A 9.9777565 9.9777565 0 0 1 14.021484 24 A 9.9777565 9.9777565 0 0 1 24 14.021484 z " />
11
+ <path
12
+ style="fill:#bf360c;fill-opacity:1;stroke:#ffffff;stroke-width:2.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
13
+ d="M 24 1.2421875 A 22.594107 22.594107 0 0 0 8.0214844 7.859375 L 17.042969 16.882812 A 9.9777565 9.9777565 0 0 1 24 14.021484 A 9.9777565 9.9777565 0 0 1 30.974609 16.867188 L 39.978516 7.8632812 A 22.594107 22.594107 0 0 0 24 1.2421875 z " />
14
+ </svg>
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ width="48"
4
+ height="48"
5
+ viewBox="0 0 48 48"
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ xmlns:svg="http://www.w3.org/2000/svg">
8
+ <path
9
+ style="fill:#bf360c;fill-opacity:1;stroke:#ffffff;stroke-width:2.50003;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
10
+ d="M 24 1.2421875 A 22.594107 22.594107 0 0 0 8.0214844 7.859375 L 17.042969 16.882812 A 9.9777565 9.9777565 0 0 1 24 14.021484 A 9.9777565 9.9777565 0 0 1 30.974609 16.867188 L 39.978516 7.8632812 A 22.594107 22.594107 0 0 0 24 1.2421875 z " />
11
+ </svg>