@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
package/src/Viewer.js ADDED
@@ -0,0 +1,1186 @@
1
+ import "./Viewer.css";
2
+ import { SYSTEM as PSSystem, DEFAULTS as PSDefaults } from "@photo-sphere-viewer/core";
3
+ import Widgets from "./viewer/Widgets";
4
+ import URLHash from "./viewer/URLHash";
5
+ import { COLORS, josmBboxParameters, linkMapAndPhoto } from "./utils/Utils";
6
+ import CoreView from "./components/CoreView";
7
+ import Photo, { PSV_DEFAULT_ZOOM, PSV_ANIM_DURATION, PIC_MAX_STAY_DURATION } from "./components/Photo";
8
+ import Map from "./components/Map";
9
+ import { TILES_PICTURES_ZOOM } from "./utils/Map";
10
+ import { enableCopyButton, fa } from "./utils/Widgets";
11
+ import { faXmark } from "@fortawesome/free-solid-svg-icons/faXmark";
12
+
13
+
14
+ const PSV_ZOOM_DELTA = 20;
15
+ const PSV_MOVE_DELTA = Math.PI / 6;
16
+ const MAP_MOVE_DELTA = 100;
17
+ const JOSM_REMOTE_URL = "http://127.0.0.1:8111";
18
+
19
+ const MAP_THEMES = {
20
+ DEFAULT: "default",
21
+ AGE: "age",
22
+ TYPE: "type",
23
+ };
24
+
25
+
26
+ /**
27
+ * Viewer is the main component of Panoramax JS library, showing pictures and map.
28
+ *
29
+ * Note that you can use any of the [CoreView](#CoreView) class functions as well.
30
+ *
31
+ * @param {string|Element} container The DOM element to create viewer into
32
+ * @param {string} endpoint URL to API to use (must be a [STAC API](https://github.com/radiantearth/stac-api-spec/blob/main/overview.md))
33
+ * @param {object} [options] Viewer options
34
+ * @param {string} [options.selectedPicture] Initial picture identifier to display
35
+ * @param {number[]} [options.position] Initial position to go to (in [lat, lon] format)
36
+ * @param {boolean} [options.hash=true] Enable URL hash settings
37
+ * @param {string} [options.lang] Override language to use (defaults to navigator language, or English if translation not available)
38
+ * @param {int} [options.transition=250] Duration of stay on a picture during sequence play (excludes loading time)
39
+ * @param {object} [options.fetchOptions=null] Set custom options for fetch calls made against API ([same syntax as fetch options parameter](https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters))
40
+ * @param {string|string[]} [options.users] The IDs of users whom data should appear on map (defaults to all). Only works with API having a "user-xyz" or "user-xyz-style" endpoint.
41
+ * @param {string} [options.picturesNavigation] The allowed navigation between pictures ("any": no restriction (default), "seq": only pictures in same sequence, "pic": only selected picture)
42
+ * @param {boolean|object} [options.map=false] Enable contextual map for locating pictures. Setting to true or passing an object enables the map. Various settings can be passed, either the ones defined here, or any of [MapLibre GL settings](https://maplibre.org/maplibre-gl-js-docs/api/map/#map-parameters)
43
+ * @param {boolean} [options.map.startWide] Show the map as main element at startup (defaults to false, viewer is wider at start)
44
+ * @param {number} [options.map.minZoom=0] The minimum zoom level of the map (0-24).
45
+ * @param {number} [options.map.maxZoom=24] The maximum zoom level of the map (0-24).
46
+ * @param {string|object} [options.style] The map's MapLibre style. This can be an a JSON object conforming to the schema described in the [MapLibre Style Specification](https://maplibre.org/maplibre-gl-js-docs/style-spec/), or a URL string pointing to one.
47
+ * @param {object} [options.map.raster] The MapLibre raster source for aerial background. This must be a JSON object following [MapLibre raster source definition](https://maplibre.org/maplibre-style-spec/sources/#raster).
48
+ * @param {external:maplibre-gl.LngLatLike} [options.map.center=[0, 0]] The initial geographical centerpoint of the map. If `center` is not specified in the constructor options, MapLibre GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `[0, 0]` Note: MapLibre GL uses longitude, latitude coordinate order (as opposed to latitude, longitude) to match GeoJSON.
49
+ * @param {number} [options.map.zoom=0] The initial zoom level of the map. If `zoom` is not specified in the constructor options, MapLibre GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`.
50
+ * @param {external:maplibre-gl.LngLatBoundsLike} [options.map.bounds] The initial bounds of the map. If `bounds` is specified, it overrides `center` and `zoom` constructor options.
51
+ * @param {object} [options.map.geocoder] Optional geocoder settings
52
+ * @param {string} [options.map.geocoder.engine] Set the geocoder engine to use (nominatim, ban)
53
+ * @param {string} [options.map.background] Choose default map background to display (streets or aerial, if raster aerial background available). Defaults to street.
54
+ * @param {string} [options.map.theme=default] The colouring scheme to use for pictures and sequences on map (default, age, type)
55
+ * @param {object} [options.widgets] Settings related to viewer buttons and widgets
56
+ * @param {string} [options.widgets.editIdUrl] URL to the OpenStreetMap iD editor (defaults to OSM.org iD instance)
57
+ * @param {string|Element} [options.widgets.customWidget] A user-defined widget to add (will be shown over "Share" button)
58
+ * @param {string} [options.widgets.mapAttribution] Override the default map attribution (read from MapLibre style)
59
+ * @param {string} [options.widgets.iframeBaseURL] Set a custom base URL for the "Share as iframe" menu (defaults to current page)
60
+ *
61
+ * @property {Map} map The map widget
62
+ * @property {Photo} psv The photo widget
63
+ */
64
+ class Viewer extends CoreView {
65
+ constructor(container, endpoint, options = {}){
66
+ super(container, endpoint, options);
67
+
68
+ if(this._options.map == null) { this._options.map = {}; }
69
+ if(this._options.widgets == null) { this._options.widgets = {}; }
70
+
71
+ // Set variables
72
+ this._sequencePlaying = false;
73
+ this._prevSequence = null;
74
+ this._mapTheme = options?.map?.theme || MAP_THEMES.DEFAULT;
75
+ this._picNav = options?.picturesNavigation || "any";
76
+
77
+ // Skip all init phases for more in-depth testing
78
+ if(this._options.testing) { return; }
79
+
80
+ // Read initial options from URL hash
81
+ let hashOpts;
82
+ if(this._options.hash === true || this._options.hash === undefined) {
83
+ this._hash = new URLHash(this);
84
+ hashOpts = this._hash._getCurrentHash();
85
+
86
+ if(hashOpts.map === "none") {
87
+ this._options.map = false;
88
+ }
89
+
90
+ if(typeof this._options.map === "object") { this._options.map.hash = false; }
91
+
92
+ // Restore focus
93
+ if(this._options.map && hashOpts.focus) {
94
+ this._options.map.startWide = hashOpts.focus === "map";
95
+ }
96
+
97
+ // Restore map background
98
+ if(this._options.map && hashOpts.background) {
99
+ this._options.map.background = hashOpts.background;
100
+ }
101
+
102
+ // Restore visible users
103
+ if(this._options.map && hashOpts.users) {
104
+ this._options.users = [...new Set(hashOpts.users.split(","))];
105
+ }
106
+
107
+ // Restore viewer position
108
+ if(hashOpts.xyz) {
109
+ const coords = this._hash.getXyzOptionsFromHashString(hashOpts.xyz);
110
+ this.addEventListener("psv:picture-loaded", () => {
111
+ this.psv.setXYZ(coords.x, coords.y, coords.z);
112
+ }, { once: true });
113
+ }
114
+
115
+ // Restore map zoom/center
116
+ if(this._options.map && typeof hashOpts.map === "string") {
117
+ const mapOpts = this._hash.getMapOptionsFromHashString(hashOpts.map);
118
+ if(mapOpts) {
119
+ this._options.map = Object.assign({}, this._options.map, mapOpts);
120
+ }
121
+ }
122
+
123
+ // Restore map filters
124
+ if(this._options.map) {
125
+ this.setFilters(this._hash.getMapFiltersFromHashVals(hashOpts), true);
126
+ }
127
+
128
+ // Restore picture from URL hash
129
+ if(hashOpts.pic) {
130
+ const picIds = hashOpts.pic.split(";"); // Handle multiple IDs coming from OSM
131
+ if(picIds.length > 1) {
132
+ console.warn("Multiple picture IDs passed in URL, only first one kept");
133
+ }
134
+ this._options.selectedPicture = picIds[0];
135
+ }
136
+
137
+ // Restore play speed
138
+ if(typeof hashOpts.speed === "string") {
139
+ this._options.transition = parseInt(hashOpts.speed);
140
+ }
141
+
142
+ // Restore pictures navigation
143
+ if(hashOpts.nav) {
144
+ this._picNav = hashOpts.nav;
145
+ }
146
+ }
147
+
148
+ // Init all DOM and components
149
+ this._initContainerStructure();
150
+ try {
151
+ this.psv = new Photo(this, this.psvContainer, {
152
+ transitionDuration: this._options.transition,
153
+ shouldGoFast: this._psvShouldGoFast.bind(this),
154
+ keyboard: "always",
155
+ keyboardActions: {
156
+ ...PSDefaults.keyboardActions,
157
+ "8": "ROTATE_UP",
158
+ "2": "ROTATE_DOWN",
159
+ "4": "ROTATE_LEFT",
160
+ "6": "ROTATE_RIGHT",
161
+
162
+ "PageUp": () => this.psv.goToNextPicture(),
163
+ "9": () => this.psv.goToNextPicture(),
164
+
165
+ "PageDown": () => this.psv.goToPrevPicture(),
166
+ "3": () => this.psv.goToPrevPicture(),
167
+
168
+ "5": () => this.moveCenter(),
169
+ "*": () => this.moveCenter(),
170
+
171
+ "Home": () => this.toggleFocus(),
172
+ "7": () => this.toggleFocus(),
173
+
174
+ "End": () => this.toggleUnfocusedVisible(),
175
+ "1": () => this.toggleUnfocusedVisible(),
176
+
177
+ " ": () => this.toggleSequencePlaying(),
178
+ "0": () => this.toggleSequencePlaying(),
179
+ },
180
+ });
181
+ this.psv.addEventListener("dblclick", () => {
182
+ if(this.map && this.isMapWide()) { this.setFocus("pic"); }
183
+ });
184
+ }
185
+ catch(e) {
186
+ let err = !PSSystem.isWebGLSupported ? this._t.gvs.error_webgl : this._t.gvs.error_psv;
187
+ this._loader.dismiss(e, err);
188
+ }
189
+
190
+ // Call appropriate functions at start according to initial options
191
+ const onceStuffReady = () => {
192
+ this._widgets = new Widgets(this, this._options.widgets);
193
+
194
+ // Hide mini component if small width
195
+ if(this.map && this.isWidthSmall()) {
196
+ this.setUnfocusedVisible(false);
197
+ }
198
+
199
+ if(this._options.selectedPicture) {
200
+ this.select(null, this._options.selectedPicture, true);
201
+ this.addEventListener("psv:picture-loaded", () => {
202
+ if(this.map && this._options.map) {
203
+ this.map.jumpTo(this._options.map);
204
+ }
205
+ if(hashOpts?.focus === "meta") {
206
+ this._widgets._showPictureMetadataPopup();
207
+ }
208
+ this._loader.dismiss();
209
+ }, { once: true });
210
+ }
211
+ else {
212
+ this._loader.dismiss();
213
+ }
214
+
215
+ if(this._options.position) {
216
+ this.goToPosition(...this._options.position).catch(e => this._loader.dismiss(e, this._t.gvs.error_nopic));
217
+ }
218
+
219
+ if(this._hash && this.map) {
220
+ this.map._attribution._container.classList.add("gvs-hidden");
221
+ this._hash.bindMapEvents();
222
+ if(this._mapFilters) {
223
+ this.setFilters(this._mapFilters, true);
224
+ }
225
+
226
+ // Restore user ID in filters
227
+ if(hashOpts.users) {
228
+ Promise.all(
229
+ this.map.getVisibleUsers()
230
+ .filter(uid => uid != "geovisio")
231
+ .map(uid => this._api.getUserName(uid))
232
+ ).then(userNames => {
233
+ userNames = userNames.filter(un => un != null).join(", ");
234
+ const userSearchField = document.getElementById("gvs-filter-search-user").querySelector("input");
235
+ if(userSearchField) {
236
+ userSearchField.setItem(userNames);
237
+ }
238
+ }).catch(e => console.warn("Error when looking up for user names", e));
239
+ }
240
+ }
241
+ };
242
+
243
+ this._api.onceReady().then(() => {
244
+ if(this._options.map) {
245
+ if(this._options.map.doubleClickZoom === undefined) {
246
+ this._options.map.doubleClickZoom = false;
247
+ }
248
+
249
+ this._initMap()
250
+ .then(onceStuffReady)
251
+ .catch(e => this._loader.dismiss(e, this._t.gvs.error_api_compatibility));
252
+ }
253
+ else {
254
+ onceStuffReady();
255
+ }
256
+ });
257
+ }
258
+
259
+ getClassName() {
260
+ return "Viewer";
261
+ }
262
+
263
+ /**
264
+ * Ends all form of life in this object.
265
+ *
266
+ * This is useful for Single Page Applications (SPA), to remove various event listeners.
267
+ */
268
+ destroy() {
269
+ super.destroy();
270
+
271
+ // Delete sub-components
272
+ this._widgets.destroy();
273
+ delete this._widgets;
274
+ this._hash.destroy();
275
+ delete this._hash;
276
+ this.map.destroy();
277
+ delete this.map;
278
+ delete this._mapFilters;
279
+ this.psv.destroy();
280
+ delete this.psv;
281
+
282
+ // Clean-up DOM
283
+ this.miniContainer.remove();
284
+ this.mainContainer.remove();
285
+ this.mapContainer.remove();
286
+ this.psvContainer.remove();
287
+ this.popupContainer.remove();
288
+ this.container.innerHTML = "";
289
+ this.container.classList.remove(...[...this.container.classList].filter(c => c.startsWith("gvs")));
290
+ }
291
+
292
+ /**
293
+ * Creates appropriate HTML elements in container to host map + viewer
294
+ *
295
+ * @private
296
+ */
297
+ _initContainerStructure() {
298
+ // Create mini-component container
299
+ this.miniContainer = document.createElement("div");
300
+ this.miniContainer.classList.add("gvs-mini");
301
+
302
+ // Create main-component container
303
+ this.mainContainer = document.createElement("div");
304
+ this.mainContainer.classList.add("gvs-main");
305
+
306
+ // Crate a popup container
307
+ this.popupContainer = document.createElement("div");
308
+ this.popupContainer.classList.add("gvs-popup", "gvs-hidden");
309
+
310
+ // Create PSV container
311
+ this.psvContainer = document.createElement("div");
312
+ this.mainContainer.appendChild(this.psvContainer);
313
+
314
+ // Create map container
315
+ this.mapContainer = document.createElement("div");
316
+ this.miniContainer.appendChild(this.mapContainer);
317
+
318
+ // Add in root container
319
+ this.container.appendChild(this.mainContainer);
320
+ this.container.appendChild(this.miniContainer);
321
+ this.container.appendChild(this.popupContainer);
322
+ }
323
+
324
+ /**
325
+ * Inits MapLibre GL component
326
+ *
327
+ * @private
328
+ * @returns {Promise} Resolves when map is ready
329
+ */
330
+ async _initMap() {
331
+ await new Promise(resolve => {
332
+ this.map = new Map(this, this.mapContainer, this._options.map);
333
+ this.map._getLayerColorStyle = this._getLayerColorStyle.bind(this);
334
+ this.map._getLayerSortStyle = this._getLayerSortStyle.bind(this);
335
+ this.addEventListener("map:users-changed", resolve, { once: true });
336
+ this.container.classList.add("gvs-has-mini");
337
+ this.map.on("dblclick", () => {
338
+ if(!this.isMapWide()) { this.setFocus("map"); }
339
+ });
340
+
341
+ if (typeof this._options.map === "object" && this._options.map.startWide) {
342
+ this.setFocus("map", true);
343
+ }
344
+ else {
345
+ this.setFocus("pic", true);
346
+ }
347
+ });
348
+
349
+ this._initMapKeyboardHandler();
350
+ linkMapAndPhoto(this);
351
+ }
352
+
353
+ /**
354
+ * Adds events related to keyboard
355
+ * @private
356
+ */
357
+ _initMapKeyboardHandler() {
358
+ const that = this;
359
+ this.map.keyboard.keydown = function(e) {
360
+ if (e.altKey || e.ctrlKey || e.metaKey) return;
361
+
362
+ // Custom keys
363
+ switch(e.key) {
364
+ case "*":
365
+ case "5":
366
+ that.moveCenter();
367
+ return;
368
+
369
+ case "PageUp":
370
+ case "9":
371
+ that.psv.goToNextPicture();
372
+ return;
373
+
374
+ case "PageDown":
375
+ case "3":
376
+ that.psv.goToPrevPicture();
377
+ return;
378
+
379
+ case "Home":
380
+ case "7":
381
+ e.stopPropagation();
382
+ that.toggleFocus();
383
+ return;
384
+
385
+ case "End":
386
+ case "1":
387
+ that.toggleUnfocusedVisible();
388
+ return;
389
+
390
+ case " ":
391
+ case "0":
392
+ that.toggleSequencePlaying();
393
+ return;
394
+ }
395
+
396
+ let zoomDir = 0;
397
+ let bearingDir = 0;
398
+ let pitchDir = 0;
399
+ let xDir = 0;
400
+ let yDir = 0;
401
+
402
+ switch (e.keyCode) {
403
+ case 61:
404
+ case 107:
405
+ case 171:
406
+ case 187:
407
+ zoomDir = 1;
408
+ break;
409
+
410
+ case 189:
411
+ case 109:
412
+ case 173:
413
+ zoomDir = -1;
414
+ break;
415
+
416
+ case 37:
417
+ case 100:
418
+ if (e.shiftKey) {
419
+ bearingDir = -1;
420
+ } else {
421
+ e.preventDefault();
422
+ xDir = -1;
423
+ }
424
+ break;
425
+
426
+ case 39:
427
+ case 102:
428
+ if (e.shiftKey) {
429
+ bearingDir = 1;
430
+ } else {
431
+ e.preventDefault();
432
+ xDir = 1;
433
+ }
434
+ break;
435
+
436
+ case 38:
437
+ case 104:
438
+ if (e.shiftKey) {
439
+ pitchDir = 1;
440
+ } else {
441
+ e.preventDefault();
442
+ yDir = -1;
443
+ }
444
+ break;
445
+
446
+ case 40:
447
+ case 98:
448
+ if (e.shiftKey) {
449
+ pitchDir = -1;
450
+ } else {
451
+ e.preventDefault();
452
+ yDir = 1;
453
+ }
454
+ break;
455
+
456
+ default:
457
+ return;
458
+ }
459
+
460
+ if (this._rotationDisabled) {
461
+ bearingDir = 0;
462
+ pitchDir = 0;
463
+ }
464
+
465
+ return {
466
+ cameraAnimation: (map) => {
467
+ const tr = this._tr;
468
+ map.easeTo({
469
+ duration: 300,
470
+ easeId: "keyboardHandler",
471
+ easing: t => t * (2-t),
472
+ zoom: zoomDir ? Math.round(tr.zoom) + zoomDir * (e.shiftKey ? 2 : 1) : tr.zoom,
473
+ bearing: tr.bearing + bearingDir * this._bearingStep,
474
+ pitch: tr.pitch + pitchDir * this._pitchStep,
475
+ offset: [-xDir * this._panStep, -yDir * this._panStep],
476
+ center: tr.center
477
+ }, {originalEvent: e});
478
+ }
479
+ };
480
+ }.bind(this.map.keyboard);
481
+ }
482
+
483
+ /**
484
+ * Given context, should tiles be loaded in PSV.
485
+ * @private
486
+ */
487
+ _psvShouldGoFast() {
488
+ return (this._sequencePlaying && this.psv.getTransitionDuration() < 1000)
489
+ || (this.map && this.isMapWide());
490
+ }
491
+
492
+ /**
493
+ * Force reload of texture and tiles in Photo Sphere Viewer.
494
+ */
495
+ refreshPSV() {
496
+ const cn = this.psv._myVTour.getCurrentNode();
497
+
498
+ // Refresh mode for flat pictures
499
+ if(cn && cn.panorama.baseUrl !== cn?.panorama?.origBaseUrl) {
500
+ const prevZoom = this.psv.getZoomLevel();
501
+ const prevPos = this.psv.getPosition();
502
+ this.psv._myVTour.state.currentNode = null;
503
+ this.psv._myVTour.setCurrentNode(cn.id, {
504
+ zoomTo: prevZoom,
505
+ rotateTo: prevPos,
506
+ fadeIn: false,
507
+ speed: 0,
508
+ rotation: false,
509
+ });
510
+ }
511
+
512
+ // Refresh mode for 360 pictures
513
+ if(cn && cn.panorama.rows > 1) {
514
+ this.psv.adapter.__refresh();
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Change full-page popup visibility and content
520
+ * @param {boolean} visible True to make it appear
521
+ * @param {string|Element[]} [content] The new popup content
522
+ */
523
+ setPopup(visible, content = null) {
524
+ if(!visible) {
525
+ this.popupContainer.classList.add("gvs-hidden");
526
+ this.setFocus("pic");
527
+ this.psv.startKeyboardControl();
528
+ }
529
+ else if(content) {
530
+ this.psv.stopKeyboardControl();
531
+ this.popupContainer.innerHTML = "";
532
+ const backdrop = document.createElement("div");
533
+ backdrop.classList.add("gvs-popup-backdrop");
534
+ backdrop.addEventListener("click", () => this.setPopup(false));
535
+ const innerDiv = document.createElement("div");
536
+ innerDiv.classList.add("gvs-widget-bg");
537
+
538
+ if(typeof content === "string") { innerDiv.innerHTML = content; }
539
+ else if(Array.isArray(content)) { content.forEach(c => innerDiv.appendChild(c)); }
540
+
541
+ // Add close button
542
+ const btnClose = document.createElement("button");
543
+ btnClose.id = "gvs-popup-btn-close";
544
+ btnClose.classList.add("gvs-btn", "gvs-widget-bg");
545
+ btnClose.appendChild(fa(faXmark));
546
+ btnClose.addEventListener("click", () => this.setPopup(false));
547
+ innerDiv.insertBefore(btnClose, innerDiv.firstChild);
548
+
549
+ this.popupContainer.appendChild(backdrop);
550
+ this.popupContainer.appendChild(innerDiv);
551
+ this.popupContainer.classList.remove("gvs-hidden");
552
+ enableCopyButton(this.popupContainer, this._t);
553
+ }
554
+ else {
555
+ this.popupContainer.classList.remove("gvs-hidden");
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Goes continuously to next picture in sequence as long as possible
561
+ */
562
+ playSequence() {
563
+ this._sequencePlaying = true;
564
+
565
+ /**
566
+ * Event for sequence starting to play
567
+ *
568
+ * @event sequence-playing
569
+ * @memberof Viewer
570
+ */
571
+ const event = new Event("sequence-playing");
572
+ this.dispatchEvent(event);
573
+
574
+ const nextPicturePlay = () => {
575
+ if(this._sequencePlaying) {
576
+ this.addEventListener("psv:picture-loaded", () => {
577
+ this._playTimer = setTimeout(() => {
578
+ nextPicturePlay();
579
+ }, this.psv.getTransitionDuration());
580
+ }, { once: true });
581
+
582
+ try {
583
+ this.psv.goToNextPicture();
584
+ }
585
+ catch(e) {
586
+ this.stopSequence();
587
+ }
588
+ }
589
+ };
590
+
591
+ // Stop playing if user clicks on image
592
+ this.psv.addEventListener("click", () => {
593
+ this.stopSequence();
594
+ });
595
+
596
+ nextPicturePlay();
597
+ }
598
+
599
+ /**
600
+ * Stops playing current sequence
601
+ */
602
+ stopSequence() {
603
+ this._sequencePlaying = false;
604
+
605
+ // Next picture timer is pending
606
+ if(this._playTimer) {
607
+ clearTimeout(this._playTimer);
608
+ delete this._playTimer;
609
+ }
610
+
611
+ // Force refresh of PSV to eventually load tiles
612
+ this.refreshPSV();
613
+
614
+ /**
615
+ * Event for sequence stopped playing
616
+ *
617
+ * @event sequence-stopped
618
+ * @memberof Viewer
619
+ */
620
+ const event = new Event("sequence-stopped");
621
+ this.dispatchEvent(event);
622
+ }
623
+
624
+ /**
625
+ * Is there any sequence being played right now ?
626
+ *
627
+ * @returns {boolean} True if sequence is playing
628
+ */
629
+ isSequencePlaying() {
630
+ return this._sequencePlaying;
631
+ }
632
+
633
+ /**
634
+ * Starts/stops the reading of pictures in a sequence
635
+ */
636
+ toggleSequencePlaying() {
637
+ if(this.isSequencePlaying()) {
638
+ this.stopSequence();
639
+ }
640
+ else {
641
+ this.playSequence();
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Move the view of main component to its center.
647
+ * For map, center view on selected picture.
648
+ * For picture, center view on image center.
649
+ */
650
+ moveCenter() {
651
+ const meta = this.psv.getPictureMetadata();
652
+ if(!meta) { return; }
653
+
654
+ if(this.map && this.isMapWide()) {
655
+ this.map.flyTo({ center: meta.gps, zoom: 20 });
656
+ }
657
+ else {
658
+ this._psvAnimate({
659
+ speed: PSV_ANIM_DURATION,
660
+ yaw: 0,
661
+ pitch: 0,
662
+ zoom: PSV_DEFAULT_ZOOM
663
+ });
664
+ }
665
+ }
666
+
667
+ /**
668
+ * Moves the view of main component slightly to the left.
669
+ */
670
+ moveLeft() {
671
+ this._moveToDirection("left");
672
+ }
673
+
674
+ /**
675
+ * Moves the view of main component slightly to the right.
676
+ */
677
+ moveRight() {
678
+ this._moveToDirection("right");
679
+ }
680
+
681
+ /**
682
+ * Moves the view of main component slightly to the top.
683
+ */
684
+ moveUp() {
685
+ this._moveToDirection("up");
686
+ }
687
+
688
+ /**
689
+ * Moves the view of main component slightly to the bottom.
690
+ */
691
+ moveDown() {
692
+ this._moveToDirection("down");
693
+ }
694
+
695
+ /**
696
+ * Moves map or picture viewer to given direction.
697
+ * @param {string} dir Direction to move to (up, left, down, right)
698
+ * @private
699
+ */
700
+ _moveToDirection(dir) {
701
+ if(this.map && this.isMapWide()) {
702
+ let pan;
703
+ switch(dir) {
704
+ case "up":
705
+ pan = [0, -MAP_MOVE_DELTA];
706
+ break;
707
+ case "left":
708
+ pan = [-MAP_MOVE_DELTA, 0];
709
+ break;
710
+ case "down":
711
+ pan = [0, MAP_MOVE_DELTA];
712
+ break;
713
+ case "right":
714
+ pan = [MAP_MOVE_DELTA, 0];
715
+ break;
716
+ }
717
+ this.map.panBy(pan);
718
+ }
719
+ else {
720
+ let pos = this.psv.getPosition();
721
+ switch(dir) {
722
+ case "up":
723
+ pos.pitch += PSV_MOVE_DELTA;
724
+ break;
725
+ case "left":
726
+ pos.yaw -= PSV_MOVE_DELTA;
727
+ break;
728
+ case "down":
729
+ pos.pitch -= PSV_MOVE_DELTA;
730
+ break;
731
+ case "right":
732
+ pos.yaw += PSV_MOVE_DELTA;
733
+ break;
734
+ }
735
+ this._psvAnimate({ speed: PSV_ANIM_DURATION, ...pos });
736
+ }
737
+ }
738
+
739
+ /**
740
+ * Overrided PSV animate function to ensure a single animation plays at once.
741
+ * @param {object} options PSV animate options
742
+ * @private
743
+ */
744
+ _psvAnimate(options) {
745
+ if(this._lastPsvAnim) { this._lastPsvAnim.cancel(); }
746
+ this._lastPsvAnim = this.psv.animate(options);
747
+ }
748
+
749
+ /**
750
+ * Is the map shown as main element instead of viewer (wide map mode) ?
751
+ *
752
+ * @returns {boolean} True if map is wider than viewer
753
+ */
754
+ isMapWide() {
755
+ if(!this.map) { throw new Error("Map is not enabled"); }
756
+ return this.mapContainer.parentNode == this.mainContainer;
757
+ }
758
+
759
+ /**
760
+ * Computes dates to use for map theme by picture/sequence age
761
+ * @private
762
+ */
763
+ _getDatesForLayerColors() {
764
+ const oneDay = 24 * 60 * 60 * 1000;
765
+ const d0 = Date.now();
766
+ const d1 = d0 - 30 * oneDay;
767
+ const d2 = d0 - 365 * oneDay;
768
+ const d3 = d0 - 2 * 365 * oneDay;
769
+ return [d1, d2, d3].map(d => new Date(d).toISOString().split("T")[0]);
770
+ }
771
+
772
+ /**
773
+ * Retrieve map layer color scheme according to selected theme.
774
+ * @private
775
+ */
776
+ _getLayerColorStyle(layer) {
777
+ // Hidden style
778
+ const s = ["case",
779
+ ["==", ["get", "hidden"], true], COLORS.HIDDEN
780
+ ];
781
+
782
+ // Selected sequence style
783
+ const picId = this.psv._myVTour?.state?.loadingNode || this.psv._myVTour?.state?.currentNode?.id;
784
+ const seqId = picId ? this.psv._picturesSequences[picId] : null;
785
+ if(layer == "sequences" && seqId) {
786
+ s.push(["==", ["get", "id"], seqId], COLORS.SELECTED);
787
+ }
788
+ else if(layer == "pictures" && seqId) {
789
+ s.push(["in", seqId, ["get", "sequences"]], COLORS.SELECTED);
790
+ }
791
+
792
+ // Themes styles
793
+ if(this._mapTheme == MAP_THEMES.AGE) {
794
+ const prop = layer == "sequences" ? "date" : "ts";
795
+ const dt = this._getDatesForLayerColors();
796
+
797
+ s.push(
798
+ ["!", ["has", prop]], COLORS.BASE,
799
+ [">=", ["get", prop], dt[0]], COLORS.PALETTE_4,
800
+ [">=", ["get", prop], dt[1]], COLORS.PALETTE_3,
801
+ [">=", ["get", prop], dt[2]], COLORS.PALETTE_2,
802
+ COLORS.PALETTE_1
803
+ );
804
+ }
805
+ else if(this._mapTheme == MAP_THEMES.TYPE) {
806
+ s.push(
807
+ ["!", ["has", "type"]], COLORS.BASE,
808
+ ["==", ["get", "type"], "equirectangular"], COLORS.QUALI_1,
809
+ COLORS.QUALI_2
810
+ );
811
+ }
812
+ else {
813
+ s.push(COLORS.BASE);
814
+ }
815
+
816
+ return s;
817
+ }
818
+
819
+ /**
820
+ * Retrieve map sort key according to selected theme.
821
+ * @private
822
+ */
823
+ _getLayerSortStyle(layer) {
824
+ // Values
825
+ // - 100 : on top / selected feature
826
+ // - 90 : hidden feature
827
+ // - 20-80 : custom ranges
828
+ // - 10 : basic feature
829
+ // - 0 : on bottom / feature with undefined property
830
+ // Hidden style
831
+ const s = ["case",
832
+ ["==", ["get", "hidden"], true], 90
833
+ ];
834
+
835
+ // Selected sequence style
836
+ const picId = this.psv._myVTour?.state?.loadingNode || this.psv._myVTour?.state?.currentNode?.id;
837
+ const seqId = picId ? this.psv._picturesSequences[picId] : null;
838
+ if(layer == "sequences" && seqId) {
839
+ s.push(["==", ["get", "id"], seqId], 100);
840
+ }
841
+ else if(layer == "pictures" && seqId) {
842
+ s.push(["in", seqId, ["get", "sequences"]], 100);
843
+ }
844
+
845
+ // Themes styles
846
+ if(this._mapTheme == MAP_THEMES.AGE) {
847
+ const prop = layer == "sequences" ? "date" : "ts";
848
+ const dt = this._getDatesForLayerColors();
849
+ s.push(
850
+ ["!", ["has", prop]], 0,
851
+ [">=", ["get", prop], dt[0]], 50,
852
+ [">=", ["get", prop], dt[1]], 49,
853
+ [">=", ["get", prop], dt[2]], 48,
854
+ );
855
+ }
856
+ else if(this._mapTheme == MAP_THEMES.TYPE) {
857
+ s.push(
858
+ ["!", ["has", "type"]], 0,
859
+ ["==", ["get", "type"], "equirectangular"], 50,
860
+ );
861
+ }
862
+
863
+ s.push(10);
864
+ return s;
865
+ }
866
+
867
+ /**
868
+ * Get current pictures navigation mode.
869
+ * @returns {string} The picture navigation mode ("any": no restriction, "seq": only pictures in same sequence, "pic": only selected picture)
870
+ */
871
+ getPicturesNavigation() {
872
+ return this._picNav;
873
+ }
874
+
875
+ /**
876
+ * Switch the allowed navigation between pictures.
877
+ * @param {string} pn The picture navigation mode ("any": no restriction, "seq": only pictures in same sequence, "pic": only selected picture)
878
+ */
879
+ setPicturesNavigation(pn) {
880
+ this._picNav = pn;
881
+
882
+ /**
883
+ * Event for pictures navigation mode change
884
+ *
885
+ * @event pictures-navigation-changed
886
+ * @memberof Viewer
887
+ * @type {object}
888
+ * @property {object} detail Event information
889
+ * @property {string} detail.value New mode (any, pic, seq)
890
+ */
891
+ const event = new CustomEvent("pictures-navigation-changed", { detail: { value: pn } });
892
+ this.dispatchEvent(event);
893
+ }
894
+
895
+ /**
896
+ * Filter function
897
+ * @param {object} link A STAC next/prev/related link definition
898
+ * @returns {boolean} True if link should be kept
899
+ * @private
900
+ */
901
+ _picturesNavFilter(link) {
902
+ switch(this._picNav) {
903
+ case "seq":
904
+ return ["next", "prev"].includes(link.rel);
905
+ case "pic":
906
+ return false;
907
+ case "any":
908
+ default:
909
+ return true;
910
+ }
911
+ }
912
+
913
+ /**
914
+ * Enable or disable JOSM live editing using [Remote](https://josm.openstreetmap.de/wiki/Help/RemoteControlCommands)
915
+ * @param {boolean} enabled Set to true to enable JOSM live
916
+ * @returns {Promise} Resolves on JOSM live being enabled or disabled
917
+ */
918
+ toggleJOSMLive(enabled) {
919
+ if(enabled) {
920
+ /**
921
+ * Event for JOSM live enabled
922
+ *
923
+ * @event josm-live-enabled
924
+ * @memberof Viewer
925
+ */
926
+ const event = new CustomEvent("josm-live-enabled");
927
+ this.dispatchEvent(event);
928
+
929
+ // Check if JOSM remote is enabled
930
+ return fetch(JOSM_REMOTE_URL+"/version")
931
+ .catch(e => {
932
+ this.dispatchEvent(new CustomEvent("josm-live-disabled"));
933
+ throw e;
934
+ })
935
+ .then(() => {
936
+ // First loading : download + zoom
937
+ const p1 = josmBboxParameters(this.psv.getPictureMetadata());
938
+ if(p1) {
939
+ const url = `${JOSM_REMOTE_URL}/load_and_zoom?${p1}`;
940
+ fetch(url).catch(e => {
941
+ console.warn(e);
942
+ this.toggleJOSMLive(false);
943
+ });
944
+ }
945
+
946
+ // Enable event listening
947
+ this._josmListener = () => {
948
+ const p2 = josmBboxParameters(this.psv.getPictureMetadata());
949
+ if(p2) {
950
+ // Next loadings : just zoom
951
+ // This avoids desktop focus to go on JOSM instead of
952
+ // staying on web browser
953
+ const url = `${JOSM_REMOTE_URL}/zoom?${p2}`;
954
+ fetch(url).catch(e => {
955
+ console.warn(e);
956
+ this.toggleJOSMLive(false);
957
+ });
958
+ }
959
+ };
960
+ this.addEventListener("psv:picture-loaded", this._josmListener);
961
+ this.addEventListener("psv:picture-loading", this._josmListener);
962
+ });
963
+ }
964
+ else {
965
+ /**
966
+ * Event for JOSM live disabled
967
+ *
968
+ * @event josm-live-disabled
969
+ * @memberof Viewer
970
+ */
971
+ const event = new CustomEvent("josm-live-disabled");
972
+ this.dispatchEvent(event);
973
+
974
+ if(this._josmListener) {
975
+ this.removeEventListener("picture-loading", this._josmListener);
976
+ this.removeEventListener("picture-loaded", this._josmListener);
977
+ delete this._josmListener;
978
+ }
979
+ return Promise.resolve();
980
+ }
981
+ }
982
+
983
+ /**
984
+ * Change the viewer focus (either on picture or map)
985
+ *
986
+ * @param {string} focus The object to focus on (map, pic)
987
+ * @param {boolean} [skipEvent=false] True to not send focus-changed event
988
+ */
989
+ setFocus(focus, skipEvent = false) {
990
+ if(focus === "map" && !this.map) { throw new Error("Map is not enabled"); }
991
+ if(!["map", "pic"].includes(focus)) { throw new Error("Invalid focus value (should be pic or map)"); }
992
+
993
+ this.mapContainer.parentElement?.removeChild(this.mapContainer);
994
+ this.psvContainer.parentElement?.removeChild(this.psvContainer);
995
+
996
+ if(focus === "map") {
997
+ this.psv.stopKeyboardControl();
998
+ this.map.keyboard.enable();
999
+ this.container.classList.add("gvs-focus-map");
1000
+ this.mainContainer.appendChild(this.mapContainer);
1001
+ this.miniContainer.appendChild(this.psvContainer);
1002
+ this.map.getCanvas().focus();
1003
+ }
1004
+ else {
1005
+ this?.map?.keyboard.disable();
1006
+ this.psv.startKeyboardControl();
1007
+ this.container.classList.remove("gvs-focus-map");
1008
+ this.mainContainer.appendChild(this.psvContainer);
1009
+ this.miniContainer.appendChild(this.mapContainer);
1010
+ this.psvContainer.focus();
1011
+ }
1012
+
1013
+ this?.map?.resize();
1014
+ this.psv.autoSize();
1015
+ this.refreshPSV();
1016
+
1017
+ if(!skipEvent) {
1018
+ /**
1019
+ * Event for focus change (either map or picture is shown wide)
1020
+ *
1021
+ * @event focus-changed
1022
+ * @memberof Viewer
1023
+ * @type {object}
1024
+ * @property {object} detail Event information
1025
+ * @property {string} detail.focus Component now focused on (map, pic)
1026
+ */
1027
+ const event = new CustomEvent("focus-changed", { detail: { focus } });
1028
+ this.dispatchEvent(event);
1029
+ }
1030
+ }
1031
+
1032
+ /**
1033
+ * Toggle the viewer focus (either on picture or map)
1034
+ */
1035
+ toggleFocus() {
1036
+ if(!this.map) { throw new Error("Map is not enabled"); }
1037
+ this.setFocus(this.isMapWide() ? "pic" : "map");
1038
+ }
1039
+
1040
+ /**
1041
+ * Change the visibility of reduced component (picture or map)
1042
+ *
1043
+ * @param {boolean} visible True to make reduced component visible
1044
+ */
1045
+ setUnfocusedVisible(visible) {
1046
+ if(!this.map) { throw new Error("Map is not enabled"); }
1047
+
1048
+ if(visible) {
1049
+ this.container.classList.remove("gvs-mini-hidden");
1050
+ }
1051
+ else {
1052
+ this.container.classList.add("gvs-mini-hidden");
1053
+ }
1054
+
1055
+ this.map.resize();
1056
+ this.psv.autoSize();
1057
+ }
1058
+
1059
+ /**
1060
+ * Toggle the visibility of reduced component (picture or map)
1061
+ */
1062
+ toggleUnfocusedVisible() {
1063
+ if(!this.map) { throw new Error("Map is not enabled"); }
1064
+ this.setUnfocusedVisible(this.container.classList.contains("gvs-mini-hidden"));
1065
+ }
1066
+
1067
+ /**
1068
+ * Change the map filters
1069
+ * @param {object} filters Filtering values
1070
+ * @param {string} [filters.minDate] Start date for pictures (format YYYY-MM-DD)
1071
+ * @param {string} [filters.maxDate] End date for pictures (format YYYY-MM-DD)
1072
+ * @param {string} [filters.type] Type of picture to keep (flat, equirectangular)
1073
+ * @param {string} [filters.camera] Camera make and model to keep
1074
+ * @param {string} [filters.theme] Map theme to use
1075
+ * @param {boolean} [skipZoomIn=false] If true, doesn't force zoom in to map level >= 7
1076
+ */
1077
+ setFilters(filters, skipZoomIn = false) {
1078
+ let mapSeqFilters = [];
1079
+ let mapPicFilters = [];
1080
+ this._mapFilters = {};
1081
+
1082
+ if(filters.minDate && filters.minDate !== "") {
1083
+ this._mapFilters.minDate = filters.minDate;
1084
+ mapSeqFilters.push([">=", ["get", "date"], filters.minDate]);
1085
+ mapPicFilters.push([">=", ["get", "ts"], filters.minDate]);
1086
+ }
1087
+ if(filters.maxDate && filters.maxDate !== "") {
1088
+ this._mapFilters.maxDate = filters.maxDate;
1089
+ mapSeqFilters.push(["<=", ["get", "date"], filters.maxDate]);
1090
+
1091
+ // Get tomorrow date for pictures filtering
1092
+ // (because ts is date+time, so comparing date only string would fail otherwise)
1093
+ let d = new Date(filters.maxDate);
1094
+ d.setDate(d.getDate() + 1);
1095
+ d = d.toISOString().split("T")[0];
1096
+ mapPicFilters.push(["<=", ["get", "ts"], d]);
1097
+ }
1098
+ if(filters.type && filters.type !== "") {
1099
+ this._mapFilters.type = filters.type;
1100
+ mapSeqFilters.push(["==", ["get", "type"], filters.type]);
1101
+ mapPicFilters.push(["==", ["get", "type"], filters.type]);
1102
+ }
1103
+ if(filters.camera && filters.camera !== "") {
1104
+ this._mapFilters.camera = filters.camera;
1105
+ // low/high model hack : to enable fuzzy filtering of camera make and model
1106
+ const lowModel = filters.camera.toLowerCase().trim() + " ";
1107
+ const highModel = filters.camera.toLowerCase().trim() + "zzzzzzzzzzzzzzzzzzzz";
1108
+ const collator = ["collator", { "case-sensitive": false, "diacritic-sensitive": false } ];
1109
+ mapSeqFilters.push([">=", ["get", "model"], lowModel, collator]);
1110
+ mapSeqFilters.push(["<=", ["get", "model"], highModel, collator]);
1111
+ mapPicFilters.push([">=", ["get", "model"], lowModel, collator]);
1112
+ mapPicFilters.push(["<=", ["get", "model"], highModel, collator]);
1113
+ }
1114
+
1115
+ if(filters.theme && Object.values(MAP_THEMES).includes(filters.theme)) {
1116
+ this._mapFilters.theme = filters.theme;
1117
+ if(this.map) {
1118
+ this._mapTheme = this._mapFilters.theme;
1119
+ this.map.reloadLayersStyles();
1120
+ }
1121
+ }
1122
+
1123
+ if(mapSeqFilters.length == 0) { mapSeqFilters = null; }
1124
+ else {
1125
+ mapSeqFilters.unshift("all");
1126
+ }
1127
+
1128
+ if(mapPicFilters.length == 0) { mapPicFilters = null; }
1129
+ else {
1130
+ mapPicFilters.unshift("all");
1131
+ mapPicFilters = ["step", ["zoom"],
1132
+ true,
1133
+ TILES_PICTURES_ZOOM, mapPicFilters
1134
+ ];
1135
+ }
1136
+
1137
+ if(this.map) {
1138
+ const allUsers = this.map.getVisibleUsers().includes("geovisio");
1139
+ if(mapSeqFilters && allUsers) {
1140
+ mapSeqFilters = ["step", ["zoom"],
1141
+ true,
1142
+ 7, mapSeqFilters
1143
+ ];
1144
+ }
1145
+
1146
+ this.map.filterUserLayersContent("sequences", mapSeqFilters);
1147
+ this.map.filterUserLayersContent("pictures", mapPicFilters);
1148
+ if(
1149
+ !skipZoomIn
1150
+ && (
1151
+ mapSeqFilters !== null
1152
+ || mapPicFilters !== null
1153
+ || (this._mapFilters.theme !== null && this._mapFilters.theme !== MAP_THEMES.DEFAULT)
1154
+ )
1155
+ && allUsers
1156
+ && this.map.getZoom() < 7
1157
+ ) {
1158
+ this.map.easeTo({ zoom: 7 });
1159
+ }
1160
+ }
1161
+
1162
+ /**
1163
+ * Event for filters changes
1164
+ *
1165
+ * @event filters-changed
1166
+ * @memberof Viewer
1167
+ * @type {object}
1168
+ * @property {object} detail Event information
1169
+ * @property {string} [detail.minDate] The minimum date in time range (ISO format)
1170
+ * @property {string} [detail.maxDate] The maximum date in time range (ISO format)
1171
+ * @property {string} [detail.type] Camera type (equirectangular, flat, null/empty string for both)
1172
+ * @property {string} [detail.camera] Camera make and model
1173
+ * @property {string} [detail.theme] Map theme
1174
+ */
1175
+ const event = new CustomEvent("filters-changed", { detail: Object.assign({}, this._mapFilters) });
1176
+ this.dispatchEvent(event);
1177
+ }
1178
+ }
1179
+
1180
+ export {
1181
+ Viewer as default, // eslint-disable-line import/no-unused-modules
1182
+ Viewer, // eslint-disable-line import/no-unused-modules
1183
+ PSV_ZOOM_DELTA,
1184
+ PSV_ANIM_DURATION,
1185
+ PIC_MAX_STAY_DURATION,
1186
+ };