@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,1196 @@
1
+ import "./Widgets.css";
2
+ import { PSV_ANIM_DURATION, PSV_ZOOM_DELTA, PIC_MAX_STAY_DURATION } from "../Viewer";
3
+ import {
4
+ createPanel, createGroup, fa, fat, createButton, disableButton,
5
+ createSearchBar, createExpandableButton, enableButton, enableCopyButton, closeOtherPanels,
6
+ createLinkCell, createTable, createHeader, createButtonSpan, createLabel
7
+ } from "../utils/Widgets";
8
+ import { COLORS, isInIframe, getUserAccount } from "../utils/Utils";
9
+ import SwitchBig from "../img/switch_big.svg";
10
+ import SwitchMini from "../img/switch_mini.svg";
11
+ import BackgroundAerial from "../img/bg_aerial.jpg";
12
+ import BackgroundStreets from "../img/bg_streets.jpg";
13
+ import { getGPSPrecision } from "../utils/Exif";
14
+
15
+ // Every single icon imported separately to reduce bundle size
16
+ import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus";
17
+ import { faMinus } from "@fortawesome/free-solid-svg-icons/faMinus";
18
+ import { faShareNodes } from "@fortawesome/free-solid-svg-icons/faShareNodes";
19
+ import { faLink } from "@fortawesome/free-solid-svg-icons/faLink";
20
+ import { faMap } from "@fortawesome/free-solid-svg-icons/faMap";
21
+ import { faImage } from "@fortawesome/free-solid-svg-icons/faImage";
22
+ import { faPanorama } from "@fortawesome/free-solid-svg-icons/faPanorama";
23
+ import { faPlay } from "@fortawesome/free-solid-svg-icons/faPlay";
24
+ import { faBackward } from "@fortawesome/free-solid-svg-icons/faBackward";
25
+ import { faForward } from "@fortawesome/free-solid-svg-icons/faForward";
26
+ import { faPause } from "@fortawesome/free-solid-svg-icons/faPause";
27
+ import { faCalendar } from "@fortawesome/free-solid-svg-icons/faCalendar";
28
+ import { faArrowRight } from "@fortawesome/free-solid-svg-icons/faArrowRight";
29
+ import { faCamera } from "@fortawesome/free-solid-svg-icons/faCamera";
30
+ import { faPen } from "@fortawesome/free-solid-svg-icons/faPen";
31
+ import { faPrint } from "@fortawesome/free-solid-svg-icons/faPrint";
32
+ import { faSatelliteDish } from "@fortawesome/free-solid-svg-icons/faSatelliteDish";
33
+ import { faEllipsisVertical } from "@fortawesome/free-solid-svg-icons/faEllipsisVertical";
34
+ import { faRocket } from "@fortawesome/free-solid-svg-icons/faRocket";
35
+ import { faPalette } from "@fortawesome/free-solid-svg-icons/faPalette";
36
+ import { faLightbulb } from "@fortawesome/free-solid-svg-icons/faLightbulb";
37
+ import { faPersonBiking } from "@fortawesome/free-solid-svg-icons/faPersonBiking";
38
+ import { faSliders } from "@fortawesome/free-solid-svg-icons/faSliders";
39
+ import { faLayerGroup } from "@fortawesome/free-solid-svg-icons/faLayerGroup";
40
+ import { faEarthEurope } from "@fortawesome/free-solid-svg-icons/faEarthEurope";
41
+ import { faUser } from "@fortawesome/free-solid-svg-icons/faUser";
42
+ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons/faCircleInfo";
43
+ import { faGear } from "@fortawesome/free-solid-svg-icons/faGear";
44
+ import { faLocationDot } from "@fortawesome/free-solid-svg-icons/faLocationDot";
45
+ import { faSquareRss } from "@fortawesome/free-solid-svg-icons/faSquareRss";
46
+ import { faCloudArrowDown } from "@fortawesome/free-solid-svg-icons/faCloudArrowDown";
47
+ import { faCopy } from "@fortawesome/free-solid-svg-icons/faCopy";
48
+ import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons/faTriangleExclamation";
49
+ import { faCircleQuestion } from "@fortawesome/free-solid-svg-icons/faCircleQuestion";
50
+ import { faCommentDots } from "@fortawesome/free-solid-svg-icons/faCommentDots";
51
+ import { faAt } from "@fortawesome/free-solid-svg-icons/faAt";
52
+ import { faPaperPlane } from "@fortawesome/free-solid-svg-icons/faPaperPlane";
53
+
54
+
55
+ /**
56
+ * Handles all map/viewer buttons visible on UI.
57
+ * Also handles switch between map and viewer, and responsiveness.
58
+ *
59
+ * @private
60
+ */
61
+ export default class Widgets {
62
+ /**
63
+ * @param {Viewer} viewer The viewer
64
+ * @param {object} [options] Widgets options
65
+ * @param {string} [options.editIdUrl] Edit with iD URL
66
+ * @param {string} [options.mapAttribution] Override default map attribution
67
+ * @param {string|Element} [options.customWidget] A user-defined widget to add
68
+ * @param {string} [options.iframeBaseURL] Set a custom base URL for the "Share as iframe" menu (defaults to current page)
69
+ */
70
+ constructor(viewer, options = {}) {
71
+ // Set default options
72
+ if(options == null) { options = {}; }
73
+ if(options.editIdUrl == null) { options.editIdUrl = "https://www.openstreetmap.org/edit"; }
74
+
75
+ this._viewer = viewer;
76
+ this._t = this._viewer._t;
77
+ this._options = options;
78
+ const hasMap = this._viewer.map !== undefined;
79
+
80
+ // Create widgets "corners"
81
+ this._corners = {};
82
+ const components = hasMap ? ["main", "mini"] : ["main"];
83
+ const cornerSpace = ["top", "bottom"];
84
+ const corners = ["left", "middle", "right"];
85
+ for(let cp of components) {
86
+ for(let cs of cornerSpace) {
87
+ const csDom = document.createElement("div");
88
+ csDom.id = `gvs-corner-${cp}-${cs}`;
89
+ csDom.classList.add("gvs-corner-space");
90
+
91
+ for(let cn of corners) {
92
+ const corner = document.createElement("div");
93
+ corner.id = `${csDom.id}-${cn}`;
94
+ corner.classList.add("gvs-corner");
95
+ this._corners[`${cp}-${cs}-${cn}`] = corner;
96
+ csDom.appendChild(corner);
97
+ }
98
+
99
+ if(cp == "main") { this._viewer.mainContainer.appendChild(csDom); }
100
+ else if(cp == "mini") { this._viewer.miniContainer.appendChild(csDom); }
101
+ }
102
+ }
103
+
104
+ if(!isInIframe()) {
105
+ this._initWidgetPlayer(hasMap);
106
+ }
107
+ this._initWidgetLegend(hasMap, options?.mapAttribution);
108
+
109
+ if(hasMap) {
110
+ this._initWidgetMiniActions();
111
+ if(!isInIframe()) {
112
+ this._initWidgetSearch();
113
+ this._initWidgetFilters(
114
+ this._viewer._api._endpoints.user_search !== null
115
+ && this._viewer._api._endpoints.user_tiles !== null
116
+ );
117
+ this._initWidgetMapLayers();
118
+ this._listenMapFiltersChanges();
119
+ }
120
+ }
121
+
122
+ if(!this._viewer.isWidthSmall()) {
123
+ this._initWidgetShare();
124
+ }
125
+
126
+ // Custom widget provided by user
127
+ if(options.customWidget) {
128
+ const corner = this._corners["main-bottom-right"];
129
+
130
+ switch(typeof options.customWidget) {
131
+ case "string":
132
+ for(let e of new DOMParser().parseFromString(options.customWidget, "text/html").body.children) {
133
+ corner.appendChild(e);
134
+ }
135
+ break;
136
+
137
+ case "object":
138
+ if(Array.isArray(options.customWidget)) {
139
+ options.customWidget.forEach(e => corner.appendChild(e));
140
+ }
141
+ else {
142
+ corner.appendChild(options.customWidget);
143
+ }
144
+ break;
145
+ }
146
+ }
147
+
148
+ this._initWidgetZoom(hasMap);
149
+
150
+ // Click outside of an open panel -> closes panels
151
+ this._viewer.container.addEventListener("click", e => closeOtherPanels(e.target, this._viewer.container));
152
+ }
153
+
154
+ /**
155
+ * Ends all form of life in this object.
156
+ */
157
+ destroy() {
158
+ Object.values(this._corners).forEach(e => e.remove());
159
+ delete this._corners;
160
+ delete this._t;
161
+ delete this._viewer;
162
+ }
163
+
164
+ /**
165
+ * Creates the zoom buttons group
166
+ * @param {boolean} hasMap True if map is enabled
167
+ * @private
168
+ */
169
+ _initWidgetZoom(hasMap) {
170
+ this._lastWantedZoom = this._viewer.psv.getZoomLevel();
171
+
172
+ // Presentation
173
+ const btnZoomIn = createButton("gvs-zoom-in", fa(faPlus), this._t.gvs.zoomIn);
174
+ const btnZoomOut = createButton("gvs-zoom-out", fa(faMinus), this._t.gvs.zoomOut);
175
+ createGroup("gvs-widget-zoom", "main-bottom-right", this, [btnZoomIn, btnZoomOut], ["gvs-group-vertical", "gvs-mobile-hidden", "gvs-print-hidden"]);
176
+
177
+ // Events
178
+ const zoomFct = (e, zoomIn) => {
179
+ if(hasMap && this._viewer.isMapWide()) {
180
+ if(zoomIn) { this._viewer.map.zoomIn({}, {originalEvent: e}); }
181
+ else { this._viewer.map.zoomOut({}, {originalEvent: e}); }
182
+ }
183
+ else {
184
+ if(this._viewer.lastPsvAnim) { this._viewer.lastPsvAnim.cancel(); }
185
+ const goToZoom = zoomIn ?
186
+ Math.min(100, this._lastWantedZoom + PSV_ZOOM_DELTA)
187
+ : Math.max(0, this._lastWantedZoom - PSV_ZOOM_DELTA);
188
+ this._viewer.lastPsvAnim = this._viewer.psv.animate({
189
+ speed: PSV_ANIM_DURATION,
190
+ zoom: goToZoom
191
+ });
192
+ this._lastWantedZoom = goToZoom;
193
+ }
194
+ };
195
+
196
+ btnZoomIn.addEventListener("click", e => zoomFct(e, true));
197
+ btnZoomOut.addEventListener("click", e => zoomFct(e, false));
198
+ }
199
+
200
+ /**
201
+ * Creates play/pause/next/prev picture buttons
202
+ * @param {boolean} hasMap True if map is enabled
203
+ * @private
204
+ */
205
+ _initWidgetPlayer(hasMap) {
206
+ // Presentation
207
+ const btnPlayerPrev = createButton("gvs-player-prev", fa(faBackward), this._t.gvs.sequence_prev);
208
+ const btnPlayerPlay = createButton("gvs-player-play");
209
+ const btnPlayerNext = createButton("gvs-player-next", fa(faForward), this._t.gvs.sequence_next);
210
+ const btnPlayerMore = createButton("gvs-player-more", fa(faEllipsisVertical), this._t.gvs.sequence_more, ["gvs-xs-hidden"]);
211
+
212
+ // Panel for more options
213
+ const pnlOpts = createPanel(this, btnPlayerMore, [], ["gvs-player-options"]);
214
+ pnlOpts.innerHTML = `
215
+ <div class="gvs-input-range" title="${this._t.gvs.sequence_speed}">
216
+ ${fat(faPersonBiking)}
217
+ <input
218
+ id="gvs-player-speed"
219
+ type="range" name="speed"
220
+ min="0" max="${PIC_MAX_STAY_DURATION - 100}"
221
+ value="${PIC_MAX_STAY_DURATION - this._viewer.psv.getTransitionDuration()}"
222
+ title="${this._t.gvs.sequence_speed}"
223
+ style="width: 100%;" />
224
+ ${fat(faRocket)}
225
+ </div>
226
+ <button title="${this._t.gvs.contrast}" id="gvs-player-contrast">
227
+ ${fat(faLightbulb)}
228
+ </button>
229
+ `;
230
+
231
+ // Group widget
232
+ const grpPlayer = createGroup(
233
+ "gvs-widget-player",
234
+ !hasMap ? "main-top-left" : "main-top-middle",
235
+ this,
236
+ [btnPlayerPrev, btnPlayerPlay, btnPlayerNext].concat(this._viewer.isWidthSmall() ? [] : [pnlOpts, btnPlayerMore]),
237
+ ["gvs-group-horizontal", "gvs-only-psv", "gvs-print-hidden", this._viewer.psv.getPictureMetadata() ? "" : "gvs-hidden"]
238
+ );
239
+
240
+ // Toggle state of play button
241
+ const toggleBtnPlay = (isPlaying) => {
242
+ btnPlayerPlay.innerHTML = isPlaying ? fat(faPause) : fat(faPlay);
243
+ btnPlayerPlay.title = isPlaying ? this._t.gvs.sequence_pause : this._t.gvs.sequence_play;
244
+ };
245
+ toggleBtnPlay(false);
246
+
247
+ // Update state of play button on picture load
248
+ const updatePlayBtn = () => {
249
+ if(this._viewer.getPicturesNavigation() === "pic") {
250
+ disableButton(btnPlayerNext);
251
+ disableButton(btnPlayerPlay);
252
+ disableButton(btnPlayerPrev);
253
+ }
254
+ else {
255
+ if(this._viewer.psv.getPictureMetadata()?.sequence?.prevPic != null) { enableButton(btnPlayerPrev); }
256
+ else { disableButton(btnPlayerPrev); }
257
+
258
+ if(this._viewer.psv.getPictureMetadata()?.sequence?.nextPic != null) {
259
+ enableButton(btnPlayerNext);
260
+ enableButton(btnPlayerPlay);
261
+ }
262
+ else {
263
+ disableButton(btnPlayerNext);
264
+ disableButton(btnPlayerPlay);
265
+ }
266
+ }
267
+ };
268
+ updatePlayBtn();
269
+
270
+ // Listening to viewer events
271
+ this._viewer.addEventListener("sequence-playing", () => toggleBtnPlay(true));
272
+ this._viewer.addEventListener("sequence-stopped", () => toggleBtnPlay(false));
273
+ this._viewer.addEventListener("psv:picture-loaded", () => grpPlayer.classList.remove("gvs-hidden"), { once: true });
274
+ this._viewer.addEventListener("psv:picture-loaded", updatePlayBtn);
275
+ this._viewer.addEventListener("pictures-navigation-changed", updatePlayBtn);
276
+
277
+ if(!this._viewer.isWidthSmall()) {
278
+ const btnPlayerSpeed = pnlOpts.children[0].children[1];
279
+
280
+ this._viewer.addEventListener("psv:transition-duration-changed", e => {
281
+ btnPlayerSpeed.value = PIC_MAX_STAY_DURATION - e.detail.value;
282
+ });
283
+
284
+ btnPlayerSpeed.addEventListener("change", e => {
285
+ const newSpeed = PIC_MAX_STAY_DURATION - e.target.value;
286
+ this._viewer.psv.setTransitionDuration(newSpeed);
287
+ });
288
+ }
289
+
290
+ // Buttons events
291
+ btnPlayerPrev.addEventListener("click", () => this._viewer.psv.goToPrevPicture());
292
+ btnPlayerNext.addEventListener("click", () => this._viewer.psv.goToNextPicture());
293
+
294
+ btnPlayerPlay.addEventListener("click", () => {
295
+ if(this._viewer.isSequencePlaying()) {
296
+ toggleBtnPlay(false);
297
+ this._viewer.stopSequence();
298
+ }
299
+ else {
300
+ toggleBtnPlay(true);
301
+ this._viewer.playSequence();
302
+ }
303
+ });
304
+
305
+ const btnPlayerContrast = document.getElementById("gvs-player-contrast");
306
+ if(btnPlayerContrast) {
307
+ btnPlayerContrast.addEventListener("click", () => {
308
+ if(btnPlayerContrast.classList.contains("gvs-btn-active")) {
309
+ btnPlayerContrast.classList.remove("gvs-btn-active");
310
+ this._viewer.psv.setHigherContrast(false);
311
+ }
312
+ else {
313
+ btnPlayerContrast.classList.add("gvs-btn-active");
314
+ this._viewer.psv.setHigherContrast(true);
315
+ }
316
+ });
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Creates legend block
322
+ * @param {boolean} hasMap True if map is enabled
323
+ * @param {string} [mapAttribution] Override map attribution
324
+ * @private
325
+ */
326
+ _initWidgetLegend(hasMap, mapAttribution) {
327
+ // Presentation (main widget)
328
+ const mainLegend = createGroup(
329
+ "gvs-widget-legend",
330
+ hasMap ? "main-bottom-right" : "main-bottom-left",
331
+ this,
332
+ [],
333
+ ["gvs-widget-bg"]
334
+ );
335
+
336
+ // Presentation (mini widget)
337
+ let miniLegend;
338
+ if(hasMap) {
339
+ miniLegend = createGroup(
340
+ "gvs-widget-mini-legend",
341
+ "mini-bottom-right",
342
+ this,
343
+ [],
344
+ ["gvs-widget-bg", "gvs-only-mini", "gvs-mobile-hidden"]
345
+ );
346
+ }
347
+
348
+ // Show/hide legend button (for small devices)
349
+ let btnVisibLegend, toggleVisibLegend;
350
+ if(this._viewer.isWidthSmall()) {
351
+ btnVisibLegend = document.createElement("button");
352
+ btnVisibLegend.id = "gvs-legend-toggle";
353
+ btnVisibLegend.classList.add("gvs-btn", "gvs-widget-bg", "gvs-print-hidden");
354
+ btnVisibLegend.appendChild(fa(faCircleInfo));
355
+ toggleVisibLegend = () => {
356
+ if(mainLegend.style.visibility === "hidden") {
357
+ mainLegend.style.visibility = "visible";
358
+ if(!hasMap) { toggleLegend(false); }
359
+ else { toggleLegend(this._viewer.isMapWide()); }
360
+ }
361
+ else {
362
+ mainLegend.innerHTML = "";
363
+ mainLegend.style.visibility = "hidden";
364
+ mainLegend.appendChild(btnVisibLegend);
365
+ }
366
+ };
367
+ btnVisibLegend.addEventListener("click", e => {
368
+ e.stopPropagation();
369
+ toggleVisibLegend();
370
+ });
371
+ }
372
+
373
+ const toggleLegend = (focusOnMap) => {
374
+ let mapLegend = mapAttribution || this._viewer.map?._attribution?._attribHTML || "";
375
+ let picLegend = "<a href='https://panoramax.fr/' target='_blank'>Panoramax</a>";
376
+
377
+ // Picture legend based on current picture metadata
378
+ const picMeta = this._viewer.psv.getPictureMetadata()?.caption;
379
+ let picMetaBtn;
380
+ if(!isInIframe() && picMeta) {
381
+ picLegend = "";
382
+ if(picMeta.producer) {
383
+ picLegend += `<span style="font-weight: bold">&copy; ${picMeta.producer}</span>`;
384
+ }
385
+ if(picMeta.date) {
386
+ if(picMeta.producer) { picLegend += "&nbsp;-&nbsp;"; }
387
+ picLegend += picMeta.date.toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" });
388
+ }
389
+
390
+ // Button for metadata popup
391
+ picMetaBtn = fa(faCircleQuestion);
392
+ picMetaBtn.style.marginLeft = "5px";
393
+ }
394
+
395
+ // Put appropriate legend according to view focus
396
+ if(focusOnMap) {
397
+ mainLegend.innerHTML = mapLegend;
398
+ if(isInIframe()) {
399
+ mainLegend.innerHTML = "<a href='https://panoramax.fr/' target='_blank'>Panoramax</a><br />" + mainLegend.innerHTML;
400
+ }
401
+ mainLegend.style.cursor = null;
402
+ mainLegend.onclick = null;
403
+ miniLegend.innerHTML = picLegend;
404
+ }
405
+ else {
406
+ mainLegend.innerHTML = picLegend;
407
+
408
+ if(picMetaBtn) {
409
+ mainLegend.appendChild(picMetaBtn);
410
+ mainLegend.style.cursor = "pointer";
411
+ mainLegend.onclick = isInIframe() ?
412
+ () => window.open(window.location.href, "_blank")
413
+ : this._showPictureMetadataPopup.bind(this);
414
+ }
415
+ else {
416
+ mainLegend.style.cursor = null;
417
+ mainLegend.onclick = null;
418
+ }
419
+
420
+ if(hasMap) { miniLegend.innerHTML = mapLegend; }
421
+ }
422
+
423
+ if(btnVisibLegend) { mainLegend.appendChild(btnVisibLegend); }
424
+ };
425
+
426
+ if(!btnVisibLegend) {
427
+ if(!hasMap) { toggleLegend(false); }
428
+ else { toggleLegend(this._viewer.isMapWide()); }
429
+ }
430
+ else {
431
+ mainLegend.appendChild(btnVisibLegend);
432
+ mainLegend.style.visibility = "hidden";
433
+ }
434
+
435
+ // Listening to viewer events
436
+ this._viewer.addEventListener("focus-changed", e => toggleLegend(e.detail.focus == "map"));
437
+ this._viewer.addEventListener("psv:picture-loaded", () => toggleLegend(hasMap && this._viewer.isMapWide()));
438
+ }
439
+
440
+ /**
441
+ * Displays current picture metadata in popup
442
+ * @private
443
+ */
444
+ _showPictureMetadataPopup() {
445
+ const picMeta = this._viewer.psv.getPictureMetadata();
446
+ if (!picMeta) { throw new Error("No picture currently selected"); }
447
+
448
+ const popupContent = [];
449
+ popupContent.push(createHeader("h4", `${fat(faCircleInfo)} ${this._t.gvs.metadata}`));
450
+
451
+ // Rapid actions (report)
452
+ if (this._viewer._api._endpoints.report) {
453
+ const popupMetaActions = createButtonSpan(`${fat(faTriangleExclamation)} ${this._t.gvs.report}`);
454
+ popupMetaActions.firstChild.addEventListener("click", this._showReportForm.bind(this));
455
+ popupContent.push(popupMetaActions);
456
+ }
457
+
458
+ // General metadata
459
+ const rowsData = [
460
+ {
461
+ section: this._t.gvs.metadata_general_picid,
462
+ classes: ["gvs-td-with-id"],
463
+ values: createLinkCell(
464
+ picMeta.id,
465
+ this._viewer._api.getPictureMetadataUrl(picMeta.id, picMeta?.sequence?.id),
466
+ this._t.gvs.metadata_general_picid_link,
467
+ this._t.gvs.copy
468
+ )
469
+ },
470
+ {
471
+ section: this._t.gvs.metadata_general_seqid,
472
+ classes: ["gvs-td-with-id"],
473
+ values: createLinkCell(
474
+ picMeta?.sequence?.id,
475
+ this._viewer._api.getSequenceMetadataUrl(picMeta?.sequence?.id),
476
+ this._t.gvs.metadata_general_seqid_link,
477
+ this._t.gvs.copy
478
+ )
479
+ },
480
+ { section: this._t.gvs.metadata_general_author, value: picMeta?.caption?.producer },
481
+ { section: this._t.gvs.metadata_general_license, value: picMeta?.caption?.license },
482
+ {
483
+ section: this._t.gvs.metadata_general_date,
484
+ value: picMeta?.caption?.date?.toLocaleDateString(undefined, {
485
+ year: "numeric", month: "long", day: "numeric",
486
+ hour: "numeric", minute: "numeric", second: "numeric",
487
+ fractionalSecondDigits: 3, timeZoneName: "short"
488
+ })
489
+ },
490
+ ];
491
+ popupContent.push(createTable("gvs-table-light", rowsData));
492
+
493
+ // Camera details
494
+ popupContent.push(createHeader("h4", `${fat(faCamera)} ${this._t.gvs.metadata_camera}`));
495
+ const focal = picMeta?.properties?.["pers:interior_orientation"]?.focal_length ? `${picMeta?.properties?.["pers:interior_orientation"]?.focal_length} mm` : "unknown";
496
+ const cameraData = [
497
+ { section: this._t.gvs.metadata_camera_make, value: picMeta?.properties?.["pers:interior_orientation"]?.camera_manufacturer },
498
+ { section: this._t.gvs.metadata_camera_model, value: picMeta?.properties?.["pers:interior_orientation"]?.camera_model },
499
+ { section: this._t.gvs.metadata_camera_type, value: picMeta?.horizontalFov === 360 ? this._t.gvs.picture_360 : this._t.gvs.picture_flat },
500
+ { section: this._t.gvs.metadata_camera_focal_length, value: focal },
501
+ ];
502
+ popupContent.push(createTable("gvs-table-light", cameraData));
503
+
504
+ // Location details
505
+ popupContent.push(createHeader("h4", `${fat(faLocationDot)} ${this._t.gvs.metadata_location}`));
506
+ const orientation = picMeta?.properties?.["view:azimuth"] !== undefined ? `${picMeta.properties["view:azimuth"]}°` : "unknown";
507
+ const gpsPrecisionLabel = getGPSPrecision(picMeta);
508
+ const locationData = [
509
+ { section: this._t.gvs.metadata_location_longitude, value: picMeta.gps[0] },
510
+ { section: this._t.gvs.metadata_location_latitude, value: picMeta.gps[1] },
511
+ { section: this._t.gvs.metadata_location_orientation, value: orientation },
512
+ { section: this._t.gvs.metadata_location_precision, value: gpsPrecisionLabel },
513
+ ];
514
+ popupContent.push(createTable("gvs-table-light", locationData));
515
+
516
+ // EXIF
517
+ if (picMeta.properties?.exif) {
518
+ const exifDetails = document.createElement("details");
519
+ exifDetails.appendChild(createHeader("summary", `${fat(faGear)} ${this._t.gvs.metadata_exif}`));
520
+
521
+ const exifData = Object.entries(picMeta.properties.exif).sort().map(([key, value]) => ({ section: key, value: value }));
522
+ exifDetails.appendChild(createTable("", exifData));
523
+ popupContent.push(exifDetails);
524
+ }
525
+
526
+ this._viewer.setPopup(true, popupContent);
527
+ this._viewer.dispatchEvent(new CustomEvent("focus-changed", { detail: { focus: "meta" } }));
528
+ }
529
+
530
+ _showReportForm() {
531
+ const picMeta = this._viewer.psv.getPictureMetadata();
532
+ if (!picMeta) { throw new Error("No picture currently selected"); }
533
+
534
+ const popupContent = [];
535
+ popupContent.push(createHeader("h4", `${fat(faTriangleExclamation)} ${this._t.gvs.report}`));
536
+
537
+ const userAccount = getUserAccount();
538
+ if(userAccount) {
539
+ const accountInfo = document.createElement("p");
540
+ accountInfo.appendChild(document.createTextNode(this._t.gvs.report_auth.replace("{a}", userAccount.name)));
541
+ popupContent.push(accountInfo);
542
+ }
543
+
544
+ const form = document.createElement("form");
545
+ popupContent.push(form);
546
+
547
+ // Nature of the issue
548
+ const issueGrp = document.createElement("div");
549
+ issueGrp.classList.add("gvs-input-group");
550
+ const issueLabel = createLabel("gvs-report-issue", this._t.gvs.report_nature_label, faCircleInfo);
551
+ const issueSelect = document.createElement("select");
552
+ issueSelect.name = "gvs-report-issue";
553
+ issueSelect.required = true;
554
+
555
+ const issueOptions = [
556
+ "", "blur_missing", "blur_excess", "inappropriate", "privacy",
557
+ "picture_low_quality", "mislocated", "copyright", "other"
558
+ ];
559
+
560
+ issueOptions.forEach(optionValue => {
561
+ const option = document.createElement("option");
562
+ option.value = optionValue;
563
+ option.textContent = this._t.gvs.report_nature[optionValue];
564
+ if(optionValue === "") {
565
+ option.setAttribute("disabled", "");
566
+ option.setAttribute("selected", "");
567
+ option.setAttribute("hidden", "");
568
+ }
569
+ issueSelect.appendChild(option);
570
+ });
571
+
572
+ issueGrp.appendChild(issueLabel);
573
+ issueGrp.appendChild(issueSelect);
574
+ form.appendChild(issueGrp);
575
+
576
+ // Picture or sequence ?
577
+ const wholeSeqGrp = document.createElement("div");
578
+ wholeSeqGrp.classList.add("gvs-input-group", "gvs-input-group-inline");
579
+ const picSeqInput = document.createElement("input");
580
+ picSeqInput.id = "gvs-report-whole-sequence";
581
+ picSeqInput.name = "gvs-report-whole-sequence";
582
+ picSeqInput.type = "checkbox";
583
+ const picSeqLabel = createLabel("gvs-report-whole-sequence", this._t.gvs.report_whole_sequence);
584
+ wholeSeqGrp.appendChild(picSeqInput);
585
+ wholeSeqGrp.appendChild(picSeqLabel);
586
+ form.appendChild(wholeSeqGrp);
587
+
588
+ // Additional details
589
+ const dtlsGrp = document.createElement("div");
590
+ dtlsGrp.classList.add("gvs-input-group");
591
+ const detailsLabel = createLabel("gvs-report-details", this._t.gvs.report_details, faCommentDots);
592
+ const detailsTextarea = document.createElement("textarea");
593
+ detailsTextarea.name = "gvs-report-details";
594
+ detailsTextarea.placeholder = this._t.gvs.report_details_placeholder;
595
+ dtlsGrp.appendChild(detailsLabel);
596
+ dtlsGrp.appendChild(detailsTextarea);
597
+ form.appendChild(dtlsGrp);
598
+
599
+ // Reporter email
600
+ let emailInput;
601
+ if(!userAccount) {
602
+ const emailGrp = document.createElement("div");
603
+ emailGrp.classList.add("gvs-input-group");
604
+ const emailLabel = createLabel("email", this._t.gvs.report_email, faAt);
605
+ emailInput = document.createElement("input");
606
+ emailInput.type = "email";
607
+ emailInput.name = "email";
608
+ emailInput.placeholder = this._t.gvs.report_email_placeholder;
609
+ emailGrp.appendChild(emailLabel);
610
+ emailGrp.appendChild(emailInput);
611
+ form.appendChild(emailGrp);
612
+ }
613
+
614
+ // Submit button
615
+ const submitGrp = document.createElement("div");
616
+ submitGrp.classList.add("gvs-input-btn");
617
+ const submitButton = document.createElement("button");
618
+ submitButton.type = "submit";
619
+ submitButton.appendChild(fa(faPaperPlane));
620
+ submitButton.appendChild(document.createTextNode(this._t.gvs.report_submit));
621
+ submitGrp.appendChild(submitButton);
622
+ form.appendChild(submitGrp);
623
+
624
+ // Submit handler
625
+ form.addEventListener("submit", e => {
626
+ e.preventDefault();
627
+
628
+ this._viewer._api.sendReport({
629
+ issue: issueSelect.value,
630
+ picture_id: picSeqInput.checked ? null : picMeta.id,
631
+ reporter_comments: detailsTextarea.value,
632
+ reporter_email: emailInput?.value,
633
+ sequence_id: picMeta.sequence.id
634
+ }).then(() => {
635
+ this._viewer.setPopup(true, [
636
+ createHeader("h4", `${fat(faTriangleExclamation)} ${this._t.gvs.report}`),
637
+ document.createTextNode(this._t.gvs.report_success)
638
+ ]);
639
+ }).catch(e => {
640
+ console.error(e);
641
+ this._viewer.setPopup(true, [
642
+ createHeader("h4", `${fat(faTriangleExclamation)} ${this._t.gvs.report}`),
643
+ document.createTextNode(this._t.gvs.report_failure.replace("{e}", e))
644
+ ]);
645
+ });
646
+ });
647
+
648
+ this._viewer.setPopup(true, popupContent);
649
+ this._viewer.dispatchEvent(new CustomEvent("focus-changed", { detail: { focus: "meta" } }));
650
+ }
651
+
652
+ /**
653
+ * Creates expand/reduce mini component.
654
+ * This should be called only if map is enabled.
655
+ * @private
656
+ */
657
+ _initWidgetMiniActions() {
658
+ // Mini widget expand
659
+ const imgExpand = document.createElement("img");
660
+ imgExpand.src = SwitchBig;
661
+ const lblExpand = document.createElement("span");
662
+ lblExpand.classList.add("gvs-mobile-hidden");
663
+ lblExpand.appendChild(document.createTextNode(this._t.gvs.expand));
664
+ const btnExpand = createButton("gvs-mini-expand", lblExpand, this._t.gvs.expand_info, ["gvs-only-mini", "gvs-print-hidden"]);
665
+ btnExpand.appendChild(imgExpand);
666
+ this._corners["mini-top-right"].appendChild(btnExpand);
667
+ btnExpand.addEventListener("click", () => {
668
+ this._viewer.setFocus(this._viewer.isMapWide() ? "pic" : "map");
669
+ });
670
+
671
+ // Mini widget hide
672
+ const imgReduce = document.createElement("img");
673
+ imgReduce.src = SwitchMini;
674
+ const btnHide = createButton("gvs-mini-hide", imgReduce, this._t.gvs.minimize, ["gvs-only-mini", "gvs-print-hidden"]);
675
+ this._corners["mini-bottom-left"].appendChild(btnHide);
676
+ btnHide.addEventListener("click", () => {
677
+ this._viewer.setUnfocusedVisible(false);
678
+ });
679
+
680
+ // Mini widget show
681
+ const btnShow = createButton("gvs-mini-show", null, null, ["gvs-btn-large", "gvs-only-mini-hidden", "gvs-print-hidden"]);
682
+ this._corners["main-bottom-left"].appendChild(btnShow);
683
+ btnShow.addEventListener("click", () => {
684
+ if(isInIframe()) {
685
+ this._viewer.setFocus(this._viewer.isMapWide() ? "pic" : "map");
686
+ }
687
+ else {
688
+ this._viewer.setUnfocusedVisible(true);
689
+ }
690
+ });
691
+
692
+ const miniBtnRendering = () => {
693
+ if(this._viewer.map && this._viewer.isMapWide()) {
694
+ btnShow.title = this._t.gvs.show_psv;
695
+ btnShow.innerHTML = fat(faPanorama);
696
+ }
697
+ else {
698
+ btnShow.title = this._t.gvs.show_map;
699
+ btnShow.innerHTML = fat(faMap);
700
+ }
701
+ };
702
+
703
+ miniBtnRendering();
704
+ this._viewer.addEventListener("focus-changed", miniBtnRendering);
705
+ }
706
+
707
+ /**
708
+ * Creates search bar component.
709
+ * This should be called only if map is enabled.
710
+ * @private
711
+ */
712
+ _initWidgetSearch() {
713
+ const geocoder = createSearchBar(
714
+ "gvs-widget-search-bar",
715
+ this._t.gvs.search_address,
716
+ (query) => this._viewer.map.geocoder({
717
+ query,
718
+ limit: 3,
719
+ bbox: this._viewer.map.getBounds().toArray().map(d => d.join(",")).join(","),
720
+ proximity: this._viewer.map.getCenter().lat+","+this._viewer.map.getCenter().lng,
721
+ }).then(data => {
722
+ data = data.features.map(f => ({
723
+ title: f.place_name.split(",")[0],
724
+ subtitle: f.place_name.split(",").slice(1).join(", "),
725
+ data: f
726
+ }));
727
+ return data;
728
+ }),
729
+ (entry) => {
730
+ if(entry) {
731
+ if(entry.data.bounds) {
732
+ this._viewer.map.fitBounds(entry.data.bounds);
733
+ }
734
+ else {
735
+ this._viewer.map.flyTo({
736
+ center: entry.data.center,
737
+ zoom: entry.data.zoom || 13,
738
+ });
739
+ }
740
+ }
741
+ },
742
+ this,
743
+ undefined,
744
+ this._viewer.isWidthSmall(),
745
+ this._viewer.map._geolocate,
746
+ );
747
+
748
+ createGroup(
749
+ "gvs-widget-search",
750
+ this._viewer.isWidthSmall() ? "main-top-right" : "main-top-left",
751
+ this,
752
+ [geocoder],
753
+ ["gvs-only-map", "gvs-print-hidden"]
754
+ );
755
+ }
756
+
757
+ /**
758
+ * Creates the map layers component.
759
+ * This should be called only if map is enabled.
760
+ * @private
761
+ */
762
+ _initWidgetMapLayers() {
763
+ const btnLayers = createExpandableButton("gvs-map-layers", faLayerGroup, this._t.gvs.layers, this);
764
+ const pnlLayers = createPanel(this, btnLayers, []);
765
+ createGroup(
766
+ "gvs-widget-map-layers",
767
+ "main-top-right",
768
+ this,
769
+ [btnLayers, pnlLayers],
770
+ ["gvs-group-large", "gvs-group-btnpanel", "gvs-only-map", "gvs-print-hidden"]
771
+ );
772
+
773
+ // Map background selector
774
+ if(this._viewer.map.hasTwoBackgrounds()) {
775
+ pnlLayers.innerHTML = `
776
+ <h4>${fat(faEarthEurope)} ${this._t.gvs.map_background}</h4>
777
+ <div id="gvs-map-bg" class="gvs-input-group">
778
+ <input type="radio" id="gvs-map-bg-streets" name="gvs-map-bg" value="streets" />
779
+ <label for="gvs-map-bg-streets">
780
+ <img id="gvs-map-bg-streets-img" />
781
+ ${this._t.gvs.map_background_streets}
782
+ </label>
783
+ <input type="radio" id="gvs-map-bg-aerial" name="gvs-map-bg" value="aerial" />
784
+ <label for="gvs-map-bg-aerial">
785
+ <img id="gvs-map-bg-aerial-img" />
786
+ ${this._t.gvs.map_background_aerial}
787
+ </label>
788
+ </div>`;
789
+ }
790
+
791
+ // Map theme selector
792
+ pnlLayers.innerHTML += `
793
+ <h4>${fat(faPalette)} ${this._t.gvs.map_theme}</h4>
794
+ <div class="gvs-input-group">
795
+ <select id="gvs-map-theme" style="width: 100%;">
796
+ <option value="default">${this._t.gvs.map_theme_default}</option>
797
+ <option value="age">${this._t.gvs.map_theme_age}</option>
798
+ <option value="type">${this._t.gvs.map_theme_type}</option>
799
+ </select>
800
+ </div>
801
+ <div>
802
+ <div id="gvs-map-theme-legend-age" class="gvs-map-theme-legend gvs-hidden">
803
+ <div>
804
+ <div class="gvs-map-theme-legend-entry">
805
+ <span class="gvs-map-theme-color" style="background-color: ${COLORS["PALETTE_4"]}"></span>
806
+ ${this._t.gvs["map_theme_age_4"]}
807
+ </div>
808
+ <div class="gvs-map-theme-legend-entry">
809
+ <span class="gvs-map-theme-color" style="background-color: ${COLORS["PALETTE_3"]}"></span>
810
+ ${this._t.gvs["map_theme_age_3"]}
811
+ </div>
812
+ </div>
813
+ <div>
814
+ <div class="gvs-map-theme-legend-entry">
815
+ <span class="gvs-map-theme-color" style="background-color: ${COLORS["PALETTE_2"]}"></span>
816
+ ${this._t.gvs["map_theme_age_2"]}
817
+ </div>
818
+ <div class="gvs-map-theme-legend-entry">
819
+ <span class="gvs-map-theme-color" style="background-color: ${COLORS["PALETTE_1"]}"></span>
820
+ ${this._t.gvs["map_theme_age_1"]}
821
+ </div>
822
+ </div>
823
+ </div>
824
+ <div id="gvs-map-theme-legend-type" class="gvs-map-theme-legend gvs-hidden">
825
+ <div class="gvs-map-theme-legend-entry">
826
+ <span class="gvs-map-theme-color" style="background-color: ${COLORS.QUALI_1}"></span>
827
+ ${this._t.gvs.picture_360}
828
+ </div>
829
+ <div class="gvs-map-theme-legend-entry">
830
+ <span class="gvs-map-theme-color" style="background-color: ${COLORS.QUALI_2}"></span>
831
+ ${this._t.gvs.picture_flat}
832
+ </div>
833
+ </div>
834
+ </div>`;
835
+
836
+ // Map theme events
837
+ const fMapTheme = pnlLayers.querySelector("#gvs-map-theme");
838
+ const onChange = () => {
839
+ this._onMapThemeChange();
840
+ this._onMapFiltersChange();
841
+ };
842
+ fMapTheme.addEventListener("change", onChange);
843
+ fMapTheme.addEventListener("keypress", onChange);
844
+ fMapTheme.addEventListener("paste", onChange);
845
+ fMapTheme.addEventListener("input", onChange);
846
+
847
+ // Map background events
848
+ if(this._viewer.map.hasTwoBackgrounds()) {
849
+ const imgBgAerial = pnlLayers.querySelector("#gvs-map-bg-aerial-img");
850
+ imgBgAerial.src = BackgroundAerial;
851
+ const imgBgStreets = pnlLayers.querySelector("#gvs-map-bg-streets-img");
852
+ imgBgStreets.src = BackgroundStreets;
853
+ const radioBgAerial = pnlLayers.querySelector("#gvs-map-bg-aerial");
854
+ const radioBgStreets = pnlLayers.querySelector("#gvs-map-bg-streets");
855
+ const onBgChange = e => {
856
+ this._viewer.map.setBackground(e.target.value);
857
+ };
858
+ radioBgAerial.addEventListener("change", onBgChange);
859
+ radioBgStreets.addEventListener("change", onBgChange);
860
+ this._viewer.addEventListener("map:background-changed", e => this._onMapBackgroundChange(e.detail.background));
861
+ this._onMapBackgroundChange(this._viewer.map.getBackground());
862
+ }
863
+ }
864
+
865
+ /**
866
+ * Change the selected background in radio buttons
867
+ * @param {string} bg The background to use
868
+ * @private
869
+ */
870
+ _onMapBackgroundChange(bg) {
871
+ const radioBgAerial = document.getElementById("gvs-map-bg-aerial");
872
+ const radioBgStreets = document.getElementById("gvs-map-bg-streets");
873
+ if(bg === "aerial") { radioBgAerial.checked = true; }
874
+ else { radioBgStreets.checked = true; }
875
+ }
876
+
877
+ /**
878
+ * Updates map theme legend when theme changes.
879
+ * @private
880
+ */
881
+ _onMapThemeChange() {
882
+ const fMapTheme = document.getElementById("gvs-map-theme");
883
+ const t = fMapTheme.value;
884
+ for(let d of document.getElementsByClassName("gvs-map-theme-legend")) {
885
+ if(d.id == "gvs-map-theme-legend-"+t) {
886
+ d.classList.remove("gvs-hidden");
887
+ }
888
+ else {
889
+ d.classList.add("gvs-hidden");
890
+ }
891
+ }
892
+ }
893
+
894
+ /**
895
+ * Creates pictures filters component.
896
+ * This should be called only if map is enabled.
897
+ * @private
898
+ */
899
+ _initWidgetFilters(hasUserSearch) {
900
+ const btnFilter = createExpandableButton("gvs-filter", faSliders, this._t.gvs.filters, this);
901
+ const pnlFilter = createPanel(this, btnFilter, []);
902
+ pnlFilter.innerHTML = `
903
+ <form id="gvs-filter-form">
904
+ <div id="gvs-filter-zoomin">${fat(faTriangleExclamation)} ${this._t.gvs.filter_zoom_in}</div>
905
+ <h4>${fat(faCalendar)} ${this._t.gvs.filter_date}</h4>
906
+ <div class="gvs-input-group">
907
+ <input type="date" id="gvs-filter-date-from" />
908
+ ${fat(faArrowRight)}
909
+ <input type="date" id="gvs-filter-date-end" />
910
+ </div>
911
+ <h4>${fat(faImage)} ${this._t.gvs.filter_picture}</h4>
912
+ <div class="gvs-input-group" style="justify-content: center;">
913
+ <input type="checkbox" id="gvs-filter-type-flat" name="flat" checked />
914
+ <label for="gvs-filter-type-flat" style="margin-right: 20px">${this._t.gvs.picture_flat}</label>
915
+ <input type="checkbox" id="gvs-filter-type-360" name="360" checked />
916
+ <label for="gvs-filter-type-360">${this._t.gvs.picture_360}</label>
917
+ </div>
918
+ <!--h4>${fat(faCamera)} ${this._t.gvs.filter_camera_model}</h4>
919
+ <div class="gvs-input-group" id="gvs-filter-model"></div-->
920
+ </form>
921
+ `;
922
+ createGroup(
923
+ "gvs-widget-filter",
924
+ this._viewer.isWidthSmall() ? "main-top-right" : "main-top-left",
925
+ this,
926
+ [btnFilter, pnlFilter],
927
+ ["gvs-group-large", "gvs-group-btnpanel", "gvs-only-map", "gvs-print-hidden"]
928
+ );
929
+
930
+ if(this._viewer.isWidthSmall()) {
931
+ pnlFilter.style.width = `${this._viewer.container.offsetWidth - 70}px`;
932
+ }
933
+
934
+ // Create search bar for users
935
+ if(hasUserSearch) {
936
+ const form = pnlFilter.querySelector("#gvs-filter-form");
937
+
938
+ const title = document.createElement("h4");
939
+ title.innerHTML = `${fat(faUser)} ${this._t.gvs.filter_user}`;
940
+ form.appendChild(title);
941
+
942
+ const input = document.createElement("div");
943
+ input.id = "gvs-filter-user";
944
+ input.classList.add("gvs-input-group");
945
+
946
+ const userSearch = createSearchBar(
947
+ "gvs-filter-search-user",
948
+ this._t.gvs.search_user,
949
+ q => this._viewer._api.searchUsers(q)
950
+ .then(data => ((data || [])
951
+ .map(f => ({
952
+ title: f.label,
953
+ data: f
954
+ }))
955
+ )),
956
+ d => this._viewer.map.setVisibleUsers(d ? [d.data.id] : ["geovisio"]),
957
+ this,
958
+ true
959
+ );
960
+ input.appendChild(userSearch);
961
+ form.appendChild(input);
962
+ }
963
+
964
+ // Create search bar for camera model
965
+ // TODO : implement when API is ready
966
+ // const cameraSearch = createSearchBar(
967
+ // "gvs-filter-camera-model",
968
+ // this._t.gvs.search,
969
+ // () => Promise.reject(),
970
+ // () => {},
971
+ // this
972
+ // );
973
+ // document.getElementById("gvs-filter-model").appendChild(cameraSearch);
974
+
975
+ const form = pnlFilter.children[0];
976
+ this._formDelay = null;
977
+
978
+ const onFormChange = () => {
979
+ if(this._formDelay) { clearTimeout(this._formDelay); }
980
+
981
+ this._formDelay = setTimeout(() => {
982
+ this._onMapFiltersChange();
983
+ }, 250);
984
+ };
985
+
986
+ form.addEventListener("change", onFormChange);
987
+ form.addEventListener("reset", onFormChange);
988
+ form.addEventListener("submit", e => {
989
+ onFormChange(e);
990
+ e.preventDefault();
991
+ return false;
992
+ }, true);
993
+
994
+ for(let i of form.getElementsByTagName("input")) {
995
+ i.addEventListener("change", onFormChange);
996
+ i.addEventListener("keypress", onFormChange);
997
+ i.addEventListener("paste", onFormChange);
998
+ i.addEventListener("input", onFormChange);
999
+ }
1000
+ }
1001
+
1002
+ /**
1003
+ * Send viewer new map filters values.
1004
+ * @private
1005
+ */
1006
+ _onMapFiltersChange() {
1007
+ const fMinDate = document.getElementById("gvs-filter-date-from");
1008
+ const fMaxDate = document.getElementById("gvs-filter-date-end");
1009
+ const fTypeFlat = document.getElementById("gvs-filter-type-flat");
1010
+ const fType360 = document.getElementById("gvs-filter-type-360");
1011
+ // const fCamera = document.getElementById("gvs-filter-camera");
1012
+ const fMapTheme = document.getElementById("gvs-map-theme");
1013
+
1014
+ let type = "";
1015
+ if(fType360.checked && !fTypeFlat.checked) { type = "equirectangular"; }
1016
+ if(!fType360.checked && fTypeFlat.checked) { type = "flat"; }
1017
+
1018
+ const values = {
1019
+ minDate: fMinDate.value,
1020
+ maxDate: fMaxDate.value,
1021
+ type,
1022
+ // camera: fCamera.value,
1023
+ theme: fMapTheme.value,
1024
+ };
1025
+
1026
+ this._viewer.setFilters(values);
1027
+ }
1028
+
1029
+ /**
1030
+ * Listen to viewer events to follow map filters changes.
1031
+ * @private
1032
+ */
1033
+ _listenMapFiltersChanges() {
1034
+ const fMinDate = document.getElementById("gvs-filter-date-from");
1035
+ const fMaxDate = document.getElementById("gvs-filter-date-end");
1036
+ const fTypeFlat = document.getElementById("gvs-filter-type-flat");
1037
+ const fType360 = document.getElementById("gvs-filter-type-360");
1038
+ // const fCamera = document.getElementById("gvs-filter-camera");
1039
+ const fMapTheme = document.getElementById("gvs-map-theme");
1040
+
1041
+ // Update widget based on programmatic filter changes
1042
+ this._viewer.addEventListener("filters-changed", e => {
1043
+ if(e.detail.minDate) { fMinDate.value = e.detail.minDate; }
1044
+ if(e.detail.maxDate) { fMaxDate.value = e.detail.maxDate; }
1045
+ // if(e.detail.camera) { fCamera.value = e.detail.camera; }
1046
+ if(e.detail.theme) { fMapTheme.value = e.detail.theme; }
1047
+ if(e.detail.type) {
1048
+ fType360.checked = ["", "equirectangular"].includes(e.detail.type);
1049
+ fTypeFlat.checked = ["", "flat"].includes(e.detail.type);
1050
+ }
1051
+ this._onMapThemeChange();
1052
+ });
1053
+
1054
+ // Show/hide zoom in warning when map zoom changes
1055
+ const lblZoomIn = document.getElementById("gvs-filter-zoomin");
1056
+ const changeLblZoomInDisplay = () => {
1057
+ if(this._viewer.map.getZoom() < 7) { lblZoomIn.style.display = null; }
1058
+ else { lblZoomIn.style.display = "none"; }
1059
+ };
1060
+ changeLblZoomInDisplay();
1061
+ this._viewer.map.on("zoomend", changeLblZoomInDisplay);
1062
+ }
1063
+
1064
+ /**
1065
+ * Creates share map/picture widget.
1066
+ * @private
1067
+ */
1068
+ _initWidgetShare() {
1069
+ const btnShare = createButton("gvs-share", fa(faShareNodes), this._t.gvs.share, ["gvs-btn-large"]);
1070
+ const pnlShare = createPanel(this, btnShare, []);
1071
+ pnlShare.innerHTML = `
1072
+ <div class="gvs-hidden">
1073
+ <p id="gvs-share-license" style="margin: 0 0 10px 0;"></p>
1074
+ </div>
1075
+ <h4 style="margin-top: 0">${fat(faLink)} ${this._t.gvs.share_links}</h4>
1076
+ <div id="gvs-share-links" class="gvs-input-btn">
1077
+ <a id="gvs-share-image" class="gvs-link-btn gvs-hidden" download target="_blank">${fat(faCloudArrowDown)} ${this._t.gvs.share_image}</a>
1078
+ <button id="gvs-share-url" data-copy="true" style="flex-basis: 100%; flex-grow: 2; flex-shrink: 2;">${fat(faCopy)} ${this._t.gvs.share_page}</button>
1079
+ <button id="gvs-share-print" style="flex-basis: 100%; flex-grow: 2; flex-shrink: 2;">${fat(faPrint)} ${this._t.gvs.share_print}</button>
1080
+ </div>
1081
+ <h4>
1082
+ ${fat(faMap)} ${this._t.gvs.share_embed}
1083
+ <a href="https://docs.panoramax.fr/web-viewer/03_URL_settings/"
1084
+ title="${this._t.gvs.share_embed_docs}"
1085
+ target="_blank"
1086
+ style="vertical-align: middle">
1087
+ ${fat(faCircleInfo)}
1088
+ </a>
1089
+ </h4>
1090
+ <div class="gvs-input-btn">
1091
+ <textarea id="gvs-share-iframe" readonly></textarea>
1092
+ <button data-input="gvs-share-iframe">${fat(faCopy)} ${this._t.gvs.copy}</button>
1093
+ </div>
1094
+ <h4 class="gvs-hidden">${fat(faPen)} ${this._t.gvs.edit_osm}</h4>
1095
+ <div class="gvs-input-btn gvs-hidden" style="justify-content: center">
1096
+ <a id="gvs-edit-id" class="gvs-link-btn" target="_blank">${fat(faLocationDot)} ${this._t.gvs.id}</a>
1097
+ <button id="gvs-edit-josm" title="${this._t.gvs.josm_live}">${fat(faSatelliteDish)} ${this._t.gvs.josm}</button>
1098
+ </div>
1099
+ `;
1100
+ createGroup(
1101
+ "gvs-widget-share",
1102
+ "main-bottom-right",
1103
+ this,
1104
+ [btnShare, pnlShare],
1105
+ ["gvs-group-large", "gvs-group-btnpanel", "gvs-mobile-hidden", "gvs-print-hidden"]
1106
+ );
1107
+
1108
+ const grpLinks = document.getElementById("gvs-share-links");
1109
+ const hdLink = document.getElementById("gvs-share-image");
1110
+ const pageLink = document.getElementById("gvs-share-url");
1111
+
1112
+ // Add RSS link if available
1113
+ if(this._viewer._api.getRSSURL()) {
1114
+ const btnRss = document.createElement("a");
1115
+ btnRss.id = "gvs-share-rss";
1116
+ btnRss.classList.add("gvs-link-btn");
1117
+ btnRss.setAttribute("target", "_blank");
1118
+ btnRss.setAttribute("title", this._t.gvs.share_rss_title);
1119
+ btnRss.appendChild(fa(faSquareRss));
1120
+ btnRss.appendChild(document.createTextNode(this._t.gvs.share_rss));
1121
+ grpLinks.insertBefore(btnRss, pageLink);
1122
+ }
1123
+
1124
+ // Update picture download links
1125
+ this._viewer.addEventListener("psv:picture-loaded", () => {
1126
+ const picMeta = this._viewer.psv.getPictureMetadata();
1127
+ hdLink.href = picMeta.panorama.hdUrl;
1128
+
1129
+ const lblLicense = document.getElementById("gvs-share-license");
1130
+ lblLicense.innerHTML = picMeta?.caption?.license ? this._t.gvs.legend_license.replace("{l}", picMeta.caption.license) : "";
1131
+
1132
+ while(pnlShare.getElementsByClassName("gvs-hidden").length > 0) {
1133
+ const h = pnlShare.getElementsByClassName("gvs-hidden")[0];
1134
+ h.classList.remove("gvs-hidden");
1135
+ }
1136
+ });
1137
+
1138
+ // Update links
1139
+ const updateLinks = e => {
1140
+ const baseUrl = e?.detail?.url || window.location.href.replace(/\/$/, "");
1141
+ const iframeBaseUrl = this._options.iframeBaseURL ?
1142
+ this._options.iframeBaseURL + window.location.hash
1143
+ : baseUrl;
1144
+ const fUrl = pnlShare.querySelector("#gvs-share-url");
1145
+ const fIframe = pnlShare.querySelector("#gvs-share-iframe");
1146
+ const btnId = pnlShare.querySelector("#gvs-edit-id");
1147
+ const btnRss = pnlShare.querySelector("#gvs-share-rss");
1148
+
1149
+ fUrl.setAttribute("data-copy", baseUrl);
1150
+ fIframe.innerText = `<iframe src="${iframeBaseUrl}" style="border: none; width: 500px; height: 300px"></iframe>`;
1151
+
1152
+ const meta = this._viewer.psv.getPictureMetadata();
1153
+ if(meta) {
1154
+ const idOpts = {
1155
+ "map": `19/${meta.gps[1]}/${meta.gps[0]}`,
1156
+ "source": "Panoramax",
1157
+ "photo_overlay": "panoramax",
1158
+ "photo": `panoramax/${meta.id}`,
1159
+ };
1160
+ btnId.setAttribute("href", `${this._options.editIdUrl}#${new URLSearchParams(idOpts).toString()}`);
1161
+ }
1162
+
1163
+ if(btnRss) {
1164
+ btnRss.setAttribute("href", this._viewer._api.getRSSURL(this._viewer?.map?.getBounds()));
1165
+ }
1166
+ };
1167
+
1168
+ updateLinks();
1169
+ this._viewer?._hash?.addEventListener("url-changed", updateLinks);
1170
+
1171
+ // Copy to clipboard on button click
1172
+ enableCopyButton(pnlShare, this._viewer._t);
1173
+
1174
+ // JOSM live edit button
1175
+ const btnJosm = pnlShare.querySelector("#gvs-edit-josm");
1176
+ btnJosm.addEventListener("click", () => {
1177
+ // Disable
1178
+ if(btnJosm.classList.contains("gvs-btn-active")) {
1179
+ this._viewer.toggleJOSMLive(false);
1180
+ }
1181
+ // Enable
1182
+ else {
1183
+ this._viewer.toggleJOSMLive(true).catch(e => {
1184
+ console.warn(e);
1185
+ alert(this._t.gvs.error_josm);
1186
+ });
1187
+ }
1188
+ });
1189
+ this._viewer.addEventListener("josm-live-enabled", () => btnJosm.classList.add("gvs-btn-active"));
1190
+ this._viewer.addEventListener("josm-live-disabled", () => btnJosm.classList.remove("gvs-btn-active"));
1191
+
1192
+ // Print button
1193
+ const printLink = pnlShare.querySelector("#gvs-share-print");
1194
+ printLink.addEventListener("click", window.print.bind(window));
1195
+ }
1196
+ }