@panoramax/web-viewer 5.0.0-develop-d7206514 → 5.0.0-develop-d26305dd

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 (152) hide show
  1. package/build/bundle.cjs +3399 -0
  2. package/build/bundle.cjs.map +1 -0
  3. package/build/bundle_photoviewer.cjs +2510 -0
  4. package/build/bundle_photoviewer.cjs.map +1 -0
  5. package/build/cjs/index.js +1 -1
  6. package/build/cjs/index_photoviewer.js +1 -1
  7. package/build/components/core/Basic.css +56 -0
  8. package/build/components/core/Basic.js +378 -0
  9. package/build/components/core/CoverageMap.css +10 -0
  10. package/build/components/core/CoverageMap.js +169 -0
  11. package/build/components/core/Editor.css +33 -0
  12. package/build/components/core/Editor.js +398 -0
  13. package/build/components/core/PhotoViewer.css +70 -0
  14. package/build/components/core/PhotoViewer.js +650 -0
  15. package/build/components/core/Viewer.css +130 -0
  16. package/build/components/core/Viewer.js +711 -0
  17. package/build/components/core/index.js +10 -0
  18. package/build/components/index.js +11 -0
  19. package/build/components/index_photoviewer.js +6 -0
  20. package/build/components/layout/BottomDrawer.js +258 -0
  21. package/build/components/layout/CorneredGrid.js +143 -0
  22. package/build/components/layout/Mini.js +121 -0
  23. package/build/components/layout/Tabs.js +140 -0
  24. package/build/components/layout/index.js +9 -0
  25. package/build/components/menus/LocationPrecisionDoc.js +42 -0
  26. package/build/components/menus/MapBackground.js +110 -0
  27. package/build/components/menus/MapFilters.js +567 -0
  28. package/build/components/menus/MapLayers.js +238 -0
  29. package/build/components/menus/MapLegend.js +68 -0
  30. package/build/components/menus/MiniPictureLegend.js +73 -0
  31. package/build/components/menus/PictureLegend.js +379 -0
  32. package/build/components/menus/PictureMetadata.js +380 -0
  33. package/build/components/menus/PlayerOptions.js +93 -0
  34. package/build/components/menus/QualityScoreDoc.js +42 -0
  35. package/build/components/menus/ReportForm.js +132 -0
  36. package/build/components/menus/SemanticsDoc.js +38 -0
  37. package/build/components/menus/SemanticsDownload.js +33 -0
  38. package/build/components/menus/SemanticsFilters.js +153 -0
  39. package/build/components/menus/SemanticsList.js +413 -0
  40. package/build/components/menus/SemanticsMetadata.js +368 -0
  41. package/build/components/menus/Share.js +105 -0
  42. package/build/components/menus/index.js +22 -0
  43. package/build/components/menus/index_photoviewer.js +11 -0
  44. package/build/components/styles.js +557 -0
  45. package/build/components/ui/AnnotationsSwitch.js +159 -0
  46. package/build/components/ui/Button.js +77 -0
  47. package/build/components/ui/ButtonGroup.css +59 -0
  48. package/build/components/ui/ButtonGroup.js +69 -0
  49. package/build/components/ui/CopyButton.js +110 -0
  50. package/build/components/ui/Grade.js +54 -0
  51. package/build/components/ui/GradeFilter.js +122 -0
  52. package/build/components/ui/IconSwitch.js +193 -0
  53. package/build/components/ui/LinkButton.js +67 -0
  54. package/build/components/ui/ListGroup.js +66 -0
  55. package/build/components/ui/ListItem.js +90 -0
  56. package/build/components/ui/Loader.js +203 -0
  57. package/build/components/ui/Map.css +63 -0
  58. package/build/components/ui/Map.js +853 -0
  59. package/build/components/ui/MapMore.js +175 -0
  60. package/build/components/ui/Photo.css +50 -0
  61. package/build/components/ui/Photo.js +1502 -0
  62. package/build/components/ui/Popup.js +145 -0
  63. package/build/components/ui/ProgressBar.js +104 -0
  64. package/build/components/ui/QualityScore.js +147 -0
  65. package/build/components/ui/SearchBar.js +374 -0
  66. package/build/components/ui/SemanticsEditor.js +191 -0
  67. package/build/components/ui/SemanticsTable.js +88 -0
  68. package/build/components/ui/Switch.js +139 -0
  69. package/build/components/ui/TogglableGroup.js +157 -0
  70. package/build/components/ui/index.js +29 -0
  71. package/build/components/ui/index_photoviewer.js +21 -0
  72. package/build/components/ui/widgets/CopyCoordinates.js +75 -0
  73. package/build/components/ui/widgets/GeoSearch.css +21 -0
  74. package/build/components/ui/widgets/GeoSearch.js +150 -0
  75. package/build/components/ui/widgets/Legend.js +190 -0
  76. package/build/components/ui/widgets/LevelSelect.css +51 -0
  77. package/build/components/ui/widgets/LevelSelect.js +143 -0
  78. package/build/components/ui/widgets/MapFiltersButton.js +114 -0
  79. package/build/components/ui/widgets/MapLayersButton.js +79 -0
  80. package/build/components/ui/widgets/OSMEditors.js +155 -0
  81. package/build/components/ui/widgets/PictureLegendActions.js +99 -0
  82. package/build/components/ui/widgets/Player.css +7 -0
  83. package/build/components/ui/widgets/Player.js +154 -0
  84. package/build/components/ui/widgets/SemanticsFiltersButton.js +65 -0
  85. package/build/components/ui/widgets/Zoom.js +84 -0
  86. package/build/components/ui/widgets/index.js +16 -0
  87. package/build/components/ui/widgets/index_photoviewer.js +7 -0
  88. package/build/esm/components/core/Basic.js +1 -1
  89. package/build/img/arrow_360.svg +14 -0
  90. package/build/img/arrow_flat.svg +11 -0
  91. package/build/img/arrow_triangle.svg +9 -0
  92. package/build/img/arrow_turn.svg +8 -0
  93. package/build/img/bg_aerial.jpg +0 -0
  94. package/build/img/bg_streets.jpg +0 -0
  95. package/build/img/loader_base.jpg +0 -0
  96. package/build/img/logo_dead.svg +91 -0
  97. package/build/img/marker.svg +17 -0
  98. package/build/img/marker_blue.svg +20 -0
  99. package/build/img/osm.svg +49 -0
  100. package/build/img/panoramax.svg +13 -0
  101. package/build/img/switch_big.svg +54 -0
  102. package/build/img/switch_mini.svg +48 -0
  103. package/build/img/wd.svg +1 -0
  104. package/build/index_photoviewer.js +4 -0
  105. package/build/package.json +148 -0
  106. package/build/servers.js +14 -0
  107. package/build/translations/ar.json +1 -0
  108. package/build/translations/be.json +257 -0
  109. package/build/translations/br.json +81 -0
  110. package/build/translations/cy.json +117 -0
  111. package/build/translations/da.json +300 -0
  112. package/build/translations/de.json +309 -0
  113. package/build/translations/en.json +294 -0
  114. package/build/translations/eo.json +235 -0
  115. package/build/translations/es.json +292 -0
  116. package/build/translations/fi.json +1 -0
  117. package/build/translations/fr.json +294 -0
  118. package/build/translations/hr.json +294 -0
  119. package/build/translations/hu.json +294 -0
  120. package/build/translations/it.json +306 -0
  121. package/build/translations/ja.json +182 -0
  122. package/build/translations/ko.json +1 -0
  123. package/build/translations/nl.json +305 -0
  124. package/build/translations/nn.json +1 -0
  125. package/build/translations/pl.json +169 -0
  126. package/build/translations/pt.json +296 -0
  127. package/build/translations/pt_BR.json +304 -0
  128. package/build/translations/sv.json +182 -0
  129. package/build/translations/ti.json +9 -0
  130. package/build/translations/tr.json +297 -0
  131. package/build/translations/uk.json +268 -0
  132. package/build/translations/zh_Hant.json +309 -0
  133. package/build/utils/API.js +928 -0
  134. package/build/utils/InitParameters.js +521 -0
  135. package/build/utils/MapStyleComposer.js +889 -0
  136. package/build/utils/PanoraMapProtocol.js +49 -0
  137. package/build/utils/PhotoAdapter.js +49 -0
  138. package/build/utils/PresetsManager.js +148 -0
  139. package/build/utils/SemanticsMapProtocol.js +144 -0
  140. package/build/utils/URLHandler.js +426 -0
  141. package/build/utils/geocoder.js +203 -0
  142. package/build/utils/i18n.js +128 -0
  143. package/build/utils/index.js +17 -0
  144. package/build/utils/index_photoviewer.js +14 -0
  145. package/build/utils/indoor.js +200 -0
  146. package/build/utils/map.js +788 -0
  147. package/build/utils/picture.js +507 -0
  148. package/build/utils/semantics.js +321 -0
  149. package/build/utils/services.js +148 -0
  150. package/build/utils/utils.js +433 -0
  151. package/build/utils/widgets.js +110 -0
  152. package/package.json +1 -1
@@ -0,0 +1,853 @@
1
+ import {
2
+ TILES_PICTURES_ZOOM, getThumbGif, isNullCoordinates,
3
+ } from "../../utils/map.js";
4
+ import SemanticsMapProtocol from "../../utils/SemanticsMapProtocol.js";
5
+ import PanoraMapProtocol from "../../utils/PanoraMapProtocol.js";
6
+ import { DISABLED_LEVEL, initIndoorEqualOnMap } from "../../utils/indoor.js";
7
+ import { svgToImg } from "../../utils/utils.js";
8
+ import MapLibreStyles from "maplibre-gl/dist/maplibre-gl.css" with { type: "css" };
9
+ import MapStyles from "./Map.css" with { type: "css" };
10
+ document.adoptedStyleSheets.push(MapLibreStyles);
11
+ document.adoptedStyleSheets.push(MapStyles);
12
+
13
+ // MapLibre imports
14
+ import "maplibre-gl";
15
+ //Import maplibreglWorker from "maplibre-gl/dist/maplibre-gl-csp-worker.js";
16
+ import * as pmtiles from "pmtiles";
17
+ //Maplibregl.workerClass = maplibreglWorker;
18
+ // eslint-disable-next-line no-undef
19
+ maplibregl.addProtocol("pmtiles", new pmtiles.Protocol().tile);
20
+ // eslint-disable-next-line no-undef
21
+ maplibregl.addProtocol("panora", new PanoraMapProtocol().tile());
22
+ // eslint-disable-next-line no-undef
23
+ maplibregl.addProtocol("panoras", new PanoraMapProtocol().tile());
24
+
25
+ const MarkerBaseSVG = await fetch(new URL("../../img/marker.svg", import.meta.url).href).then(res => res.text());
26
+ const MarkerSelectedSVG = await fetch(new URL("../../img/marker_blue.svg", import.meta.url).href).then(res => res.text());
27
+
28
+
29
+ /**
30
+ * Map is the component showing pictures and sequences geolocation.
31
+ *
32
+ * Note that all functions of [MapLibre GL JS class Map](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/) are also available.
33
+ *
34
+ * A more complete version of Map (with filters & themes) is available through [MapMore class](#Panoramax.components.ui.MapMore)
35
+ *
36
+ * ℹ️ Many style management is done in [MapStyleComposer](#Panoramax.utils.MapStyleComposer), which is available as a property of parent component.
37
+ *
38
+ * ⚠️ This class doesn't inherit from [EventTarget](https://developer.mozilla.org/fr/docs/Web/API/EventTarget), so it doesn't have `addEventListener` and `dispatchEvent` functions.
39
+ * It uses instead [`on`](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/#on) and `fire` functions from MapLibre Map class.
40
+ * `fire` function doesn't take directly [`Event`](https://developer.mozilla.org/fr/docs/Web/API/Event) objects, but a string and object data.
41
+ * A shorthand `addEventListener` function is added for simpler usage.
42
+ * @class Panoramax.components.ui.Map
43
+ * @extends [maplibregl.Map](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/)
44
+ * @param {Panoramax.components.core.Basic} parent The parent view
45
+ * @param {Element} container The DOM element to create into
46
+ * @param {object} [options] The map options (any of [MapLibre GL settings](https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/MapOptions/) or any supplementary option defined here)
47
+ * @param {object} [options.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 {object} [options.basemaps] List of complementary basemaps, as { id: {MapLibre Style JSON or URL} }. Note that special IDs streets and aerial are reserved for default backgrounds.
49
+ * @param {string} [options.background=streets] Choose default map background to display (streets or aerial, if raster aerial background available). Defaults to street.
50
+ * @param {string} [options.attributionControl.customAttribution] To override default map attribution.
51
+ * @param {boolean} [options.picMarkerDraggable] To make the picture marker draggable, default to false.
52
+ * @param {object} [options.indoor] The indoor= MapLibre plugin options. This must be a JSON object following [IndoorEqual parameters](https://indoorequal.com/doc/maplibre-gl-indoorequal/api#parameters). Note that this is only available if [maplibre-gl-indoorequal](https://indoorequal.com/doc/maplibre-gl-indoorequal) plugin is loaded in your web page.
53
+ * @param {string} [options.indoor.level] (only if indoor= plugin is enabled) The initial indoor level to display. Defaults to ground or disabled depending on indoor map availability. Do not use if visible=false.
54
+ * @param {boolean} [options.indoor.visible=true] (only if indoor= plugin is enabled) The initial indoor visibility. Set to false to avoid data loading before user explicitly enables indoor through widgets. If set to false, you may not set any level option in component.
55
+ * @fires Panoramax.components.ui.Map#sequence-hover
56
+ * @fires Panoramax.components.ui.Map#sequence-click
57
+ * @fires Panoramax.components.ui.Map#picture-click
58
+ * @fires Panoramax.components.ui.Map#ready
59
+ * @example
60
+ * const map = new Panoramax.components.ui.Map(viewer, mapNode, {center: {lat: 48.7, lng: -1.7}});
61
+ */
62
+ // eslint-disable-next-line no-undef
63
+ export default class Map extends maplibregl.Map {
64
+ constructor(parent, container, options = {}) {
65
+ super({
66
+ container: container,
67
+ style: parent.mapStyleComposer.getMapStyle(),
68
+ center: [0,0],
69
+ zoom: 0,
70
+ maxZoom: 24,
71
+ attributionControl: false,
72
+ dragRotate: false,
73
+ pitchWithRotate: false,
74
+ touchZoomRotate: true,
75
+ touchPitch: false,
76
+ doubleClickZoom: false,
77
+ canvasContextAttributes: {
78
+ preserveDrawingBuffer: !parent.isWidthSmall(),
79
+ },
80
+ transformRequest: parent.api._getMapRequestTransform(),
81
+ locale: parent._t.maplibre,
82
+ hash: false,
83
+ ...options
84
+ });
85
+
86
+ this._semMapProtocol = new SemanticsMapProtocol(parent);
87
+ // eslint-disable-next-line no-undef
88
+ maplibregl.addProtocol("sem", this._semMapProtocol.tile());
89
+ this._loadMarkerImages();
90
+
91
+ this._parent = parent;
92
+ this._parent.mapStyleComposer.map = this;
93
+
94
+ this._options = options;
95
+ this.getContainer().classList.add("pnx-map");
96
+
97
+ // Disable touch rotate
98
+ if(options.touchZoomRotate === undefined) {
99
+ this?.touchZoomRotate?.disableRotation();
100
+ }
101
+
102
+ // Handle raster source
103
+ if(this._options.raster) {
104
+ this._options.background = this._options.background || "streets";
105
+ }
106
+ // Supplementary basemaps
107
+ if(this._options.basemaps) {
108
+ this._parent.mapStyleComposer._createManyBasemaps(this._options.basemaps, this._options.background);
109
+ }
110
+
111
+ initIndoorEqualOnMap(this, ![undefined, DISABLED_LEVEL].includes(this._options.level));
112
+
113
+ // eslint-disable-next-line no-undef
114
+ this._attribution = new maplibregl.AttributionControl({ compact: false, ...options.attributionControl });
115
+ this.addControl(this._attribution);
116
+
117
+ this._initMapPosition();
118
+
119
+ // Widgets and markers
120
+ this._picMarker = this._getPictureMarker();
121
+ this._picMarkerPreview = this._getPictureMarker(false);
122
+
123
+ // Popup
124
+ this._picPreviewTimer = null;
125
+ // eslint-disable-next-line no-undef
126
+ this._picPopup = new maplibregl.Popup({
127
+ closeButton: false,
128
+ closeOnClick: !this._parent.isWidthSmall(),
129
+ offset: 15
130
+ });
131
+
132
+ // Cache for pictures and sequences thumbnails
133
+ this._picThumbUrl = {};
134
+ this._seqPictures = {};
135
+
136
+ // Parent selection
137
+ this._parent.addEventListener("select", this._parent.mapStyleComposer.updateMapStyle.bind(this._parent.mapStyleComposer));
138
+
139
+ // Post styles loading handlers
140
+ this._parent.mapStyleComposer.addEventListener("panoramax-changed", this._onPanoramaxChanged.bind(this));
141
+
142
+ // Timeout for initial loading
143
+ setTimeout(() => {
144
+ if(!this.loaded() && this._parent?.loader.isVisible()) {
145
+ this._parent.loader.dismiss({}, this._parent._t.map.slow_loading, async () => {
146
+ await this._postLoad();
147
+ this._parent.loader.dismiss();
148
+ });
149
+ }
150
+ }, 15000);
151
+
152
+ this.waitForEnoughMapLoaded().then(async () => await this._postLoad());
153
+ }
154
+
155
+ /**
156
+ * @private
157
+ */
158
+ _postLoad() {
159
+ this.resize();
160
+
161
+ // Map background click
162
+ this.on("dblclick", () => clearTimeout(this._gridFocus));
163
+ this.on("click", (e) => {
164
+ clearTimeout(this._gridFocus);
165
+
166
+ if(e.defaultPrevented === false) {
167
+ this._dropPreview();
168
+ if(this._parent.isMapWide?.() && !this._parent._hasMenuOpened()) {
169
+ this._gridFocus = setTimeout(() => this._parent?.grid?.toggleAway(), 100);
170
+ }
171
+ }
172
+ });
173
+
174
+ // Untoggle away widgets
175
+ const untoggleGridWidgets = () => {
176
+ if(this._parent.isMapWide?.() && this._parent?.grid?._hidden) {
177
+ // No clearTimeout as map continuously send move events
178
+ this._gridFocus = setTimeout(() => this._parent?.grid?.toggleAway(true), 100);
179
+ }
180
+ };
181
+ this.on("mousemove", untoggleGridWidgets);
182
+ this.on("touchmove", untoggleGridWidgets);
183
+
184
+ /**
185
+ * Event when map is ready to display.
186
+ * This includes Maplibre initial load, enough map data display and styling.
187
+ * @event Panoramax.components.ui.Map#ready
188
+ * @type {maplibregl.util.evented.Event}
189
+ */
190
+ this.fire("ready");
191
+ }
192
+
193
+ /** @private */
194
+ _dropPreview(delayed) {
195
+ clearTimeout(this._picDropPreviewTimer);
196
+ if(delayed) {
197
+ this._picDropPreviewTimer = setTimeout(this._dropPreview.bind(this), 250);
198
+ }
199
+ else {
200
+ clearTimeout(this._picPreviewTimer);
201
+ delete this._picPopup._picId;
202
+ delete this._picPopup._loading;
203
+ this._picPopup.remove();
204
+ this._picMarkerPreview.remove();
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Destroy any form of life in this component
210
+ * @memberof Panoramax.components.ui.Map#
211
+ */
212
+ destroy() {
213
+ this.remove();
214
+ delete this._semMapProtocol;
215
+ delete this._parent;
216
+ delete this._options;
217
+ delete this._attribution;
218
+ delete this._picMarker;
219
+ delete this._picMarkerPreview;
220
+ delete this._picPreviewTimer;
221
+ delete this._picPopup;
222
+ delete this._picThumbUrl;
223
+ delete this._seqPictures;
224
+ }
225
+
226
+ /**
227
+ * Helper to know when enough map background and Panoramax tiles are loaded for a proper display.
228
+ * @returns {Promise} Resolves when enough is loaded
229
+ * @memberof Panoramax.components.ui.Map#
230
+ */
231
+ waitForEnoughMapLoaded() {
232
+ return new Promise((resolve) => {
233
+ let nbBgTiles = 0;
234
+ let nbFgTiles = 0;
235
+ let nbLoadedBgTiles = 0;
236
+ let nbLoadedFgTiles = 0;
237
+
238
+ const onSourceDataLoading = e => {
239
+ if(e.dataType === "source" && e.tile) {
240
+ if(e.sourceId.startsWith("geovisio")) {
241
+ nbFgTiles++;
242
+ }
243
+ else {
244
+ nbBgTiles++;
245
+ }
246
+ }
247
+ };
248
+ const onSourceData = e => {
249
+ if(e.dataType === "source" && e.tile) {
250
+ if(e.sourceId.startsWith("geovisio")) {
251
+ nbLoadedFgTiles++;
252
+ if(e.isSourceLoaded) { nbLoadedFgTiles = nbFgTiles; }
253
+ }
254
+ else {
255
+ nbLoadedBgTiles++;
256
+ if(e.isSourceLoaded) { nbLoadedBgTiles = nbBgTiles; }
257
+ }
258
+ }
259
+ checkEnoughLoaded();
260
+ };
261
+
262
+ const checkEnoughLoaded = () => {
263
+ if(nbLoadedBgTiles / nbBgTiles >= 0.75 && (nbFgTiles === 0 || nbLoadedFgTiles / nbFgTiles >= 0.75)) {
264
+ this.off("sourcedata", onSourceData);
265
+ this.off("sourcedataloading", onSourceDataLoading);
266
+ resolve();
267
+ }
268
+ };
269
+
270
+ this.on("sourcedataloading", onSourceDataLoading);
271
+ this.on("sourcedata", onSourceData);
272
+ });
273
+ }
274
+
275
+ /**
276
+ * Sets map view based on returned API bbox (if no precise option given by user).
277
+ * @private
278
+ * @memberof Panoramax.components.ui.Map#
279
+ */
280
+ _initMapPosition() {
281
+ if(
282
+ isNullCoordinates(this._options.center)
283
+ && (!this._options.zoom || this._options.zoom === 0)
284
+ && (!this._options.hash)
285
+ ) {
286
+ this._parent.onceAPIReady().then(() => {
287
+ let bbox = this._parent?.api?.getDataBbox();
288
+ if(bbox) {
289
+ try {
290
+ // eslint-disable-next-line no-undef
291
+ bbox = new maplibregl.LngLatBounds(bbox);
292
+ if(this.loaded()) { this.fitBounds(bbox, { "animate": false }); }
293
+ else { this.waitForEnoughMapLoaded().then(() => this.fitBounds(bbox, { "animate": false })); }
294
+ }
295
+ catch(e) {
296
+ console.warn("Received invalid bbox: "+bbox);
297
+ }
298
+ }
299
+ });
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Adds a new source.
305
+ * This is an override of MapLibre's function to catch sources missing in MapStyleComposer
306
+ */
307
+ addSource(id, source) {
308
+ this._parent.mapStyleComposer.registerSource(id, source);
309
+ super.addSource(id, source);
310
+ }
311
+
312
+ /**
313
+ * Adds a new map layer.
314
+ * This is an override of MapLibre's function to catch layers missing in MapStyleComposer
315
+ */
316
+ addLayer(layer, beforeId) {
317
+ this._parent.mapStyleComposer.registerLayer(layer, beforeId);
318
+ super.addLayer(layer, beforeId);
319
+ }
320
+
321
+ /**
322
+ * Load markers into map for use in map layers.
323
+ * @private
324
+ * @memberof Panoramax.components.ui.Map#
325
+ */
326
+ _loadMarkerImages() {
327
+ [
328
+ { id: "pnx-marker", img: MarkerBaseSVG },
329
+ ].forEach(m => {
330
+ const img = new Image(64, 64);
331
+ img.onload = () => this.addImage(m.id, img);
332
+ svgToImg(m.img, img);
333
+ });
334
+ }
335
+
336
+ /**
337
+ * Is Quality Score available in vector tiles ?
338
+ * @private
339
+ * @memberof Panoramax.components.ui.Map#
340
+ */
341
+ _hasQualityScore() {
342
+ const fields = this.getStyle()?.metadata?.["panoramax:fields"] || {};
343
+ return fields?.pictures?.includes("gps_accuracy") && fields?.pictures?.includes("h_pixel_density");
344
+ }
345
+
346
+ /**
347
+ * Are 360/flat pictures stats available in vector tiles for grid layer ?
348
+ * @private
349
+ * @memberof Panoramax.components.ui.Map#
350
+ */
351
+ _hasGridStats() {
352
+ const fields = this.getStyle()?.metadata?.["panoramax:fields"] || {};
353
+ return fields?.grid?.includes("nb_360_pictures") && fields?.grid?.includes("nb_flat_pictures")
354
+ && fields?.grid?.includes("coef_360_pictures") && fields?.grid?.includes("coef_flat_pictures");
355
+ }
356
+
357
+ /**
358
+ * Are specific 360/flat pictures stats available for logged-in users in vector tiles for grid layer ?
359
+ * @private
360
+ * @memberof Panoramax.components.ui.Map#
361
+ */
362
+ _hasLoggedGridStats() {
363
+ const fields = this.getStyle()?.metadata?.["panoramax:fields"] || {};
364
+ return fields?.grid?.includes("logged_coef_360_pictures")
365
+ && fields?.grid?.includes("logged_coef_flat_pictures")
366
+ && fields?.grid?.includes("logged_coef");
367
+ }
368
+
369
+ /**
370
+ * Force refresh of vector tiles data
371
+ * @memberof Panoramax.components.ui.Map#
372
+ */
373
+ reloadVectorTiles() {
374
+ if(!this._parent.mapStyleComposer?.layerRanges?.panoramax) { return; }
375
+
376
+ Object
377
+ .values(this._parent.mapStyleComposer.layerRanges.panoramax)
378
+ .forEach(p => {
379
+ Object.keys(p.sources).forEach(sid => {
380
+ const sm = this.getSource(sid);
381
+ if(sm) { sm.setTiles(sm.tiles); }
382
+ });
383
+ });
384
+ }
385
+
386
+ /**
387
+ * Shows on map a picture position and heading.
388
+ *
389
+ * If no longitude & latitude are set, marker is removed from map.
390
+ * @memberof Panoramax.components.ui.Map#
391
+ * @param {number} lon The longitude
392
+ * @param {number} lat The latitude
393
+ * @param {number} heading The heading
394
+ * @param {boolean} [skipCenter=false] Set to true to avoid map centering on marker
395
+ * @param {string} [picId=null] The picture Id
396
+ */
397
+ displayPictureMarker(lon, lat, heading, skipCenter = false, picId = null) {
398
+ this._picMarkerPreview.remove();
399
+
400
+ // Show marker corresponding to selection
401
+ if(lon !== undefined && lat !== undefined) {
402
+ this._picMarker
403
+ .setLngLat([lon, lat])
404
+ .setRotation(heading)
405
+ .addTo(this);
406
+ this._picMarker.picId = picId ;
407
+ }
408
+ else {
409
+ this._picMarker.remove();
410
+ }
411
+
412
+ // Update map style to see selected sequence
413
+ this._parent.mapStyleComposer.updateMapStyle();
414
+
415
+ // Move map to picture coordinates
416
+ if(!skipCenter && lon !== undefined && lat !== undefined) {
417
+ this.jumpTo({ // No animation to avoid conflict on seq=* load
418
+ center: [lon, lat],
419
+ zoom: this.getZoom() < TILES_PICTURES_ZOOM+2 ? TILES_PICTURES_ZOOM+2 : this.getZoom(),
420
+ });
421
+ }
422
+ }
423
+
424
+ /** @private */
425
+ _onPanoramaxChanged() {
426
+ const msc = this._parent.mapStyleComposer;
427
+ const picLayerId = msc.layerRanges.panoramax[msc.panoramax].layers.find(l => l.id.endsWith("_pictures"))?.id;
428
+ const seqPlusLayerId = msc.layerRanges.panoramax[msc.panoramax].layers.find(l => l.id.endsWith("_sequences_plus"))?.id;
429
+ const gridLayerId = msc.layerRanges.panoramax[msc.panoramax].layers.find(l => l.id.endsWith("_grid"))?.id;
430
+ if(!picLayerId || !seqPlusLayerId) { console.error("Panoramax source not loaded"); }
431
+
432
+ // Pictures
433
+ this.on("mousemove", picLayerId, e => {
434
+ this.getCanvas().style.cursor = "pointer";
435
+ const eCopy = Object.assign({}, e);
436
+ this._attachPreviewToPictures(eCopy, picLayerId);
437
+ });
438
+
439
+ this.on("mouseleave", picLayerId, () => {
440
+ this.getCanvas().style.cursor = "";
441
+ });
442
+
443
+ this.on("click", picLayerId, this._onPictureClick.bind(this));
444
+
445
+ // Sequences
446
+ this.on("mousemove", seqPlusLayerId, e => {
447
+ if(this.getZoom() >= TILES_PICTURES_ZOOM) { return; }
448
+ this._onSequenceHover(e);
449
+ this.getCanvas().style.cursor = "pointer";
450
+ if(e.features[0].properties.id) {
451
+ const eCopy = Object.assign({}, e);
452
+ this._attachPreviewToPictures(eCopy, seqPlusLayerId);
453
+ }
454
+ });
455
+
456
+ this.on("mouseleave", seqPlusLayerId, () => {
457
+ this.getCanvas().style.cursor = "";
458
+ this._dropPreview(true);
459
+ });
460
+
461
+ this.on("click", seqPlusLayerId, e => {
462
+ e.preventDefault();
463
+ this._onSequenceClick(e);
464
+ });
465
+
466
+ // Grid
467
+ if(gridLayerId) {
468
+ this.on("mousemove", gridLayerId, e => {
469
+ if(this.getZoom() <= TILES_PICTURES_ZOOM+1) {
470
+ this.getCanvas().style.cursor = "pointer";
471
+ const eCopy = Object.assign({}, e);
472
+ this._attachPreviewToPictures(eCopy, gridLayerId);
473
+ }
474
+ });
475
+
476
+ this.on("mouseleave", gridLayerId, () => {
477
+ this._dropPreview();
478
+ this.getCanvas().style.cursor = "";
479
+ });
480
+
481
+ this.on("click", gridLayerId, e => {
482
+ e.preventDefault();
483
+ this.flyTo({ center: e.lngLat, zoom: TILES_PICTURES_ZOOM-6 });
484
+ });
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Creates popup manager for preview of pictures.
490
+ * @private
491
+ * @param {object} e The event thrown by MapLibre
492
+ * @param {string} from The event source layer
493
+ * @memberof Panoramax.components.ui.Map#
494
+ */
495
+ _attachPreviewToPictures(e, from) {
496
+ let f = e.features.pop();
497
+ // eslint-disable-next-line eqeqeq
498
+ if(!f || f.properties.id == this._picPopup._picId) { return; }
499
+
500
+ clearTimeout(this._picPreviewTimer);
501
+ clearTimeout(this._picDropPreviewTimer);
502
+ this._picPopup._loading = f.properties.id;
503
+ this._picPopup._picId = f.properties.id;
504
+ delete this._picPopup._seqId;
505
+
506
+ if(!e.lngLat) { e.lngLat = this.getCenter(); }
507
+
508
+
509
+ /*
510
+ * Display picture preview marker
511
+ */
512
+
513
+ let picPreview = null;
514
+ if(from.includes("_pictures") && f.geometry.type === "Point") { // From picture hover
515
+ picPreview = f;
516
+ }
517
+ else if(this.getZoom() < TILES_PICTURES_ZOOM && from.includes("_sequences") && f.geometry.type === "LineString") { // From sequence hover
518
+ // Look for nearest sequence point
519
+ let prevDist = null;
520
+ for(let i=0; i < f.geometry.coordinates.length; i++) {
521
+ // eslint-disable-next-line no-undef
522
+ let dist = e.lngLat.distanceTo(new maplibregl.LngLat(...f.geometry.coordinates[i]));
523
+ if(!prevDist || dist < prevDist) {
524
+ picPreview = {
525
+ type: "Feature",
526
+ geometry: { type: "Point", coordinates: f.geometry.coordinates[i] },
527
+ properties: {
528
+ first_sequence: f.properties.id,
529
+ collection: f.properties.id,
530
+ }
531
+ };
532
+ prevDist = dist;
533
+ }
534
+ }
535
+ }
536
+
537
+ if(picPreview && picPreview.properties.heading !== undefined) {
538
+ this._picPopup._seqId = picPreview.properties.first_sequence;
539
+ this._picMarkerPreview
540
+ .setLngLat?.(picPreview.geometry.coordinates)
541
+ .setRotation(picPreview.properties.heading || 0)
542
+ .addTo(this);
543
+ }
544
+ else { this._picMarkerPreview?.remove?.(); }
545
+
546
+
547
+ /*
548
+ * Display thumbnail popup
549
+ */
550
+
551
+ let thumbCoords = picPreview ? picPreview.geometry.coordinates : e.lngLat;
552
+ this._picPopup
553
+ .setLngLat(thumbCoords)
554
+ .addTo(this);
555
+
556
+ // Only show GIF loader if thumbnail is not in browser cache
557
+ if(!picPreview && !this._picThumbUrl[f.properties.id]) {
558
+ this._picPopup.setDOMContent(getThumbGif(this._parent._t));
559
+ }
560
+
561
+ const displayThumb = thumbUrl => {
562
+ if(this._picPopup._loading === f.properties.id) {
563
+ if(thumbUrl) {
564
+ let content = document.createElement("img");
565
+ content.classList.add("pnx-map-thumb");
566
+ content.alt = this._parent._t.map.thumbnail;
567
+ let img = new Image();
568
+ img.src = thumbUrl;
569
+
570
+ img.addEventListener("load", () => {
571
+ if(this._picPopup._loading !== f.properties.id) { return; }
572
+
573
+ if(f.properties.hidden) {
574
+ content.children[0].src = img.src;
575
+ }
576
+ else {
577
+ content.src = img.src;
578
+ }
579
+ this._picPopup.setDOMContent(content);
580
+ delete this._picPopup._loading;
581
+ });
582
+
583
+ if(f.properties.hidden) {
584
+ const legend = document.createElement("div");
585
+ legend.classList.add("pnx-map-thumb-legend");
586
+ legend.appendChild(document.createTextNode(this._parent._t.map.not_public));
587
+ const container = document.createElement("div");
588
+ container.appendChild(content);
589
+ container.appendChild(legend);
590
+ content = container;
591
+ }
592
+ }
593
+ else {
594
+ this._picPopup.remove();
595
+ }
596
+ }
597
+ };
598
+
599
+ if(this._picPopup._loading !== f.properties.id) { return; }
600
+
601
+ // Hover on cached thumbnail
602
+ if(this._picThumbUrl[picPreview?.properties?.id || f.properties.id]) {
603
+ displayThumb(this._picThumbUrl[picPreview?.properties?.id || f.properties.id]);
604
+ }
605
+ // Throttled call to API
606
+ else {
607
+ clearTimeout(this._picPreviewTimer);
608
+ this._picPreviewTimer = setTimeout(() => {
609
+ if(this._picPopup._loading !== f.properties.id) { return; }
610
+
611
+ // Hover on a single picture
612
+ if(picPreview) {
613
+ if(picPreview.properties.id) {
614
+ this._getPictureThumbURL(picPreview.properties.id, picPreview.properties.collection).then(displayThumb);
615
+ }
616
+ else {
617
+ this._getSequenceThumbURL(f.properties.collection, thumbCoords).then(displayThumb);
618
+ }
619
+ }
620
+ // Hover on a grid cell
621
+ else if(from.endsWith("_grid")) {
622
+ this._getThumbURL(thumbCoords).then(displayThumb);
623
+ }
624
+ // Hover on a sequence
625
+ else if(from.includes("_sequences")) {
626
+ this._getSequenceThumbURL(f.properties.id, thumbCoords).then(displayThumb);
627
+ }
628
+ }, 50);
629
+ }
630
+ }
631
+
632
+ /**
633
+ * Get picture thumbnail URL at given coordinates
634
+ *
635
+ * @param {LngLat} coordinates The map coordinates
636
+ * @returns {Promise} Promise resolving on picture thumbnail URL, or null on timeout
637
+ * @private
638
+ * @memberof Panoramax.components.ui.Map#
639
+ */
640
+ _getThumbURL(coordinates) {
641
+ return this._parent.getAPI().getPicturesAroundCoordinates(coordinates.lat, coordinates.lng, 0.1, 1).then(res => {
642
+ const p = res?.features?.pop();
643
+ return p ? this._parent.getAPI().findThumbnailInPictureFeature(p) : null;
644
+ });
645
+ }
646
+
647
+
648
+ /**
649
+ * Get picture thumbnail URL for a given sequence ID
650
+ *
651
+ * @param {string} seqId The sequence ID
652
+ * @param {LngLat} [coordinates] The map coordinates
653
+ * @returns {Promise} Promise resolving on picture thumbnail URL, or null on timeout
654
+ * @private
655
+ * @memberof Panoramax.components.ui.Map#
656
+ */
657
+ _getSequenceThumbURL(seqId, coordinates) {
658
+ if(coordinates) {
659
+ return this._parent.getAPI().getPicturesAroundCoordinates(coordinates.lat || coordinates[1], coordinates.lng || coordinates[0], 1, 1, seqId)
660
+ .then(results => {
661
+ if(results?.features?.length > 0) {
662
+ return this._parent.getAPI().findThumbnailInPictureFeature(results.features[0]);
663
+ }
664
+ else {
665
+ return this._parent.getAPI().getPictureThumbnailURLForSequence(seqId);
666
+ }
667
+ });
668
+ }
669
+ else {
670
+ return this._parent.getAPI().getPictureThumbnailURLForSequence(seqId);
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Get picture thumbnail URL for a given picture ID.
676
+ * It handles a client-side cache based on raw API responses.
677
+ *
678
+ * @param {string} picId The picture ID
679
+ * @param {string} [seqId] The sequence ID (can speed up search if available)
680
+ * @returns {Promise} Promise resolving on picture thumbnail URL, or null on timeout
681
+ * @memberof Panoramax.components.ui.Map#
682
+ * @private
683
+ */
684
+ _getPictureThumbURL(picId, seqId) {
685
+ let res = Promise.resolve();
686
+
687
+ if(picId) {
688
+ if(this._picThumbUrl[picId] !== undefined) {
689
+ res = typeof this._picThumbUrl[picId] === "string" ? Promise.resolve(this._picThumbUrl[picId]) : this._picThumbUrl[picId];
690
+ }
691
+ else {
692
+ this._picThumbUrl[picId] = this._parent.getAPI().getPictureThumbnailURL(picId, seqId).then(url => {
693
+ if(url) {
694
+ this._picThumbUrl[picId] = url;
695
+ return url;
696
+ }
697
+ else {
698
+ this._picThumbUrl[picId] = null;
699
+ return null;
700
+ }
701
+ })
702
+ .catch(() => {
703
+ this._picThumbUrl[picId] = null;
704
+ });
705
+ res = this._picThumbUrl[picId];
706
+ }
707
+ }
708
+
709
+ return res;
710
+ }
711
+
712
+ /**
713
+ * Create a ready-to-use picture marker
714
+ *
715
+ * @returns {maplibregl.Marker} The generated marker
716
+ * @private
717
+ * @memberof Panoramax.components.ui.Map#
718
+ */
719
+ _getPictureMarker(selected = true) {
720
+ // eslint-disable-next-line no-undef
721
+ return new maplibregl.Marker({
722
+ element: svgToImg(selected ? MarkerSelectedSVG : MarkerBaseSVG),
723
+ picId: null
724
+ })
725
+ // Only picMarker could be draggable, don't for picMarkerPreview.
726
+ .setDraggable(selected && this._options.picMarkerDraggable)
727
+ ;
728
+ }
729
+
730
+ /**
731
+ * Event handler for sequence hover
732
+ * @private
733
+ * @param {object} e Event data
734
+ * @memberof Panoramax.components.ui.Map#
735
+ */
736
+ _onSequenceHover(e) {
737
+ e.preventDefault();
738
+ if(e.features.length > 0 && e.features[0].properties?.id) {
739
+ /**
740
+ * Event when a sequence on map is hovered (not selected)
741
+ *
742
+ * @event Panoramax.components.ui.Map#sequence-hover
743
+ * @type {maplibregl.util.evented.Event}
744
+ * @property {string} seqId The hovered sequence ID
745
+ */
746
+ this.fire("sequence-hover", { seqId: e.features[0].properties.id });
747
+ }
748
+ }
749
+
750
+ /**
751
+ * Event handler for sequence click
752
+ * @private
753
+ * @param {object} e Event data
754
+ * @memberof Panoramax.components.ui.Map#
755
+ */
756
+ _onSequenceClick(e) {
757
+ e.preventDefault();
758
+ e.originalEvent.stopImmediatePropagation();
759
+
760
+ // Skip if pictures navigation is set to "pic"
761
+ if(this._parent.psv?.getPicturesNavigation() === "pic") {
762
+ return;
763
+ }
764
+
765
+ if(e.features.length > 0) {
766
+ let seq = e.features[0];
767
+
768
+ // If clicks on many sequences, prefer the one corresponding to picture thumbnail preview
769
+ if(e.features.length > 1 && this._picPopup._seqId) {
770
+ seq = e.features.find(s => s.properties.id === this._picPopup._seqId) || seq;
771
+ }
772
+
773
+ if(seq.properties?.id) {
774
+ /**
775
+ * Event when a sequence on map is clicked
776
+ * @event Panoramax.components.ui.Map#sequence-click
777
+ * @type {maplibregl.util.evented.Event}
778
+ * @property {string} seqId The clicked sequence ID
779
+ * @property {maplibregl.LngLat} coordinates The coordinates of user click
780
+ */
781
+ this.fire("sequence-click", {
782
+ seqId: seq.properties.id,
783
+ coordinates: e.lngLat
784
+ });
785
+ }
786
+ }
787
+ }
788
+
789
+ /**
790
+ * Event handler for picture click
791
+ * @private
792
+ * @param {object} e Event data
793
+ * @memberof Panoramax.components.ui.Map#
794
+ */
795
+ _onPictureClick(e) {
796
+ e.preventDefault();
797
+ e.originalEvent.stopImmediatePropagation();
798
+
799
+ // Skip if pictures navigation is set to "pic"
800
+ if(this._parent.psv?.getPicturesNavigation() === "pic") {
801
+ return;
802
+ }
803
+
804
+ // Skip if click happens as well on a semantic layer
805
+ const features = this.queryRenderedFeatures(e.point);
806
+ if(features.length > 1) {
807
+ if(features.find(f => f?.source?.startsWith("sem-"))) { return; }
808
+ }
809
+
810
+ const f = e?.features?.length > 0 ? e.features[0] : null;
811
+ if(f?.properties?.id) {
812
+ // Look for a potential sequence ID
813
+ let seqId = null;
814
+ try {
815
+ if(f.properties.sequences) {
816
+ if(!Array.isArray(f.properties.sequences)) { f.properties.sequences = JSON.parse(f.properties.sequences); }
817
+ seqId = f.properties.sequences.pop();
818
+ }
819
+ }
820
+ catch(e) {
821
+ console.log("Sequence ID is not available in vector tiles for picture "+f.properties.id);
822
+ }
823
+
824
+ /**
825
+ * Event when a picture on map is clicked
826
+ *
827
+ * @event Panoramax.components.ui.Map#picture-click
828
+ * @type {maplibregl.util.evented.Event}
829
+ * @property {string} picId The clicked picture ID
830
+ * @property {string} seqId The clicked picture's sequence ID
831
+ * @property {object} feature The GeoJSON feature of the picture
832
+ */
833
+ this.fire("picture-click", {
834
+ picId: f.properties.id,
835
+ seqId,
836
+ feature: f
837
+ });
838
+ }
839
+ }
840
+
841
+ /**
842
+ * Listen to map events.
843
+ * This is a binder to [`on`](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/#on) and [`once`](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/#once) MapLibre GL functions.
844
+ * @param {string} type The event type to listen for
845
+ * @param {function} listener The event handler
846
+ * @param {boolean} [options.once=false] Set to true to only listen to first event.
847
+ * @memberof Panoramax.components.ui.Map#
848
+ */
849
+ addEventListener(type, listener, options = {}) {
850
+ if(options?.once) { this.once(type, listener); }
851
+ else { this.on(type, listener); }
852
+ }
853
+ }