@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/Editor.js ADDED
@@ -0,0 +1,359 @@
1
+ import "./Editor.css";
2
+ import CoreView from "./components/CoreView";
3
+ import Map from "./components/Map";
4
+ import Photo from "./components/Photo";
5
+ import BackgroundAerial from "./img/bg_aerial.jpg";
6
+ import BackgroundStreets from "./img/bg_streets.jpg";
7
+ import { linkMapAndPhoto, apiFeatureToPSVNode } from "./utils/Utils";
8
+ import { VECTOR_STYLES } from "./utils/Map";
9
+ import { SYSTEM as PSSystem } from "@photo-sphere-viewer/core";
10
+
11
+ const LAYER_HEADING_ID = "sequence-headings";
12
+
13
+ /**
14
+ * Editor allows to focus on a single sequence, and preview what you edits would look like.
15
+ * It shows both picture and map.
16
+ *
17
+ * Note that you can use any of the [CoreView](#CoreView) class functions as well.
18
+ *
19
+ * @param {string|Element} container The DOM element to create viewer into
20
+ * @param {string} endpoint URL to API to use (must be a [STAC API](https://github.com/radiantearth/stac-api-spec/blob/main/overview.md))
21
+ * @param {object} [options] View options.
22
+ * @param {string} options.selectedSequence The ID of sequence to highlight on load. Must be always defined.
23
+ * @param {string} [options.selectedPicture] The ID of picture to highlight on load (defaults to none)
24
+ * @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))
25
+ * @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).
26
+ * @param {string} [options.background] Choose default map background to display (streets or aerial, if raster aerial background available). Defaults to street.
27
+ * @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.
28
+ *
29
+ * @property {Map} map The map widget
30
+ * @property {Photo} psv The photo widget
31
+ */
32
+ export default class Editor extends CoreView {
33
+ constructor(container, endpoint, options = {}){
34
+ super(container, endpoint, Object.assign(options, { users: [] }));
35
+
36
+ // Check sequence ID is set
37
+ if(!this._selectedSeqId) { this._loader.dismiss({}, "No sequence is selected"); }
38
+
39
+ // Create sub-containers
40
+ this.psvContainer = document.createElement("div");
41
+ this.mapContainer = document.createElement("div");
42
+ this.container.appendChild(this.psvContainer);
43
+ this.container.appendChild(this.mapContainer);
44
+
45
+ // Init PSV
46
+ try {
47
+ this.psv = new Photo(this, this.psvContainer);
48
+ this.psv._myVTour.datasource.nodeResolver = this._getNode.bind(this);
49
+ }
50
+ catch(e) {
51
+ let err = !PSSystem.isWebGLSupported ? this._t.gvs.error_webgl : this._t.gvs.error_psv;
52
+ this._loader.dismiss(e, err);
53
+ }
54
+
55
+ // Init map
56
+ this._api.onceReady().then(() => {
57
+ try {
58
+ this.map = new Map(this, this.mapContainer, {
59
+ raster: options.raster,
60
+ background: options.background,
61
+ supplementaryStyle: this._createMapStyle(),
62
+ });
63
+ linkMapAndPhoto(this);
64
+ this._loadSequence();
65
+ this.map.once("load", () => {
66
+ this.map.setPaintProperty("geovisio_editor_sequences", "line-color", this.map._getLayerColorStyle("sequences"));
67
+ this.map.setPaintProperty("geovisio_editor_pictures", "circle-color", this.map._getLayerColorStyle("pictures"));
68
+ this.map.setLayoutProperty("geovisio_editor_sequences", "visibility", "visible");
69
+ this.map.setLayoutProperty("geovisio_editor_pictures", "visibility", "visible");
70
+ if(options.raster) { this._addMapBackgroundWidget(); }
71
+ this._bindPicturesEvents();
72
+ });
73
+
74
+ // Override picMarker setRotation for heading preview
75
+ const oldRot = this.map._picMarker.setRotation.bind(this.map._picMarker);
76
+ this.map._picMarker.setRotation = h => {
77
+ h = this._lastRelHeading === undefined ? h : h + this._lastRelHeading - this.psv.getPictureRelativeHeading();
78
+ return oldRot(h);
79
+ };
80
+ }
81
+ catch(e) {
82
+ this._loader.dismiss(e, this._t.gvs.error_psv);
83
+ }
84
+ });
85
+
86
+ // Events
87
+ this.addEventListener("select", this._onSelect.bind(this));
88
+ }
89
+
90
+ getClassName() {
91
+ return "Editor";
92
+ }
93
+
94
+ /**
95
+ * Create style for GeoJSON sequence data.
96
+ * @private
97
+ */
98
+ _createMapStyle() {
99
+ return {
100
+ sources: {
101
+ geovisio_editor_sequences: {
102
+ type: "geojson",
103
+ data: {"type": "FeatureCollection", "features": [] }
104
+ }
105
+ },
106
+ layers: [
107
+ {
108
+ "id": "geovisio_editor_sequences",
109
+ "type": "line",
110
+ "source": "geovisio_editor_sequences",
111
+ "layout": {
112
+ ...VECTOR_STYLES.SEQUENCES.layout
113
+ },
114
+ "paint": {
115
+ ...VECTOR_STYLES.SEQUENCES.paint
116
+ },
117
+ },
118
+ {
119
+ "id": "geovisio_editor_pictures",
120
+ "type": "circle",
121
+ "source": "geovisio_editor_sequences",
122
+ "layout": {
123
+ ...VECTOR_STYLES.PICTURES.layout
124
+ },
125
+ "paint": {
126
+ ...VECTOR_STYLES.PICTURES.paint
127
+ },
128
+ }
129
+ ]
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Creates events handlers on pictures layer
135
+ * @private
136
+ */
137
+ _bindPicturesEvents() {
138
+ // Pictures events
139
+ this.map.on("mousemove", "geovisio_editor_pictures", () => {
140
+ this.map.getCanvas().style.cursor = "pointer";
141
+ });
142
+
143
+ this.map.on("mouseleave", "geovisio_editor_pictures", () => {
144
+ this.map.getCanvas().style.cursor = "";
145
+ });
146
+
147
+ this.map.on("click", "geovisio_editor_pictures", this.map._onPictureClick.bind(this.map));
148
+ }
149
+
150
+ /**
151
+ * Displays currently selected sequence on map
152
+ * @private
153
+ */
154
+ _loadSequence() {
155
+ return this._api.getSequenceItems(this._selectedSeqId).then(seq => {
156
+ // Create data source
157
+ this._sequenceData = seq.features;
158
+ this.map.getSource("geovisio_editor_sequences").setData({
159
+ "type": "FeatureCollection",
160
+ "features": [
161
+ {
162
+ "type": "Feature",
163
+ "properties": {
164
+ "id": this._selectedSeqId,
165
+ },
166
+ "geometry":
167
+ {
168
+ "type": "LineString",
169
+ "coordinates": seq.features.map(p => p.geometry.coordinates)
170
+ }
171
+ },
172
+ ...seq.features.map(f => {
173
+ f.properties.id = f.id;
174
+ f.properties.sequences = [this._selectedSeqId];
175
+ return f;
176
+ })
177
+ ]
178
+ });
179
+
180
+ const onMapLoad = () => {
181
+ // Select picture if any
182
+ if(this._selectedPicId) {
183
+ const pic = seq.features.find(p => p.id === this._selectedPicId);
184
+ if(pic) {
185
+ this.select(this._selectedSeqId, this._selectedPicId, true);
186
+ this.map.jumpTo({ center: pic.geometry.coordinates, zoom: 18 });
187
+ }
188
+ else {
189
+ console.log("Picture with ID", pic, "was not found");
190
+ }
191
+ }
192
+ // Show area of sequence otherwise
193
+ else {
194
+ const bbox = [
195
+ ...seq.features[0].geometry.coordinates,
196
+ ...seq.features[0].geometry.coordinates
197
+ ];
198
+
199
+ for(let i=1; i < seq.features.length; i++) {
200
+ const c = seq.features[i].geometry.coordinates;
201
+ if(c[0] < bbox[0]) { bbox[0] = c[0]; }
202
+ if(c[1] < bbox[1]) { bbox[1] = c[1]; }
203
+ if(c[0] > bbox[2]) { bbox[2] = c[0]; }
204
+ if(c[1] > bbox[3]) { bbox[3] = c[1]; }
205
+ }
206
+
207
+ this.map.fitBounds(bbox, {animate: false});
208
+ }
209
+ this._loader.dismiss();
210
+ };
211
+
212
+ if(this.map.loaded()) { onMapLoad(); }
213
+ else { this.map.once("load", onMapLoad); }
214
+ }).catch(e => this._loader.dismiss(e, this._t.gvs.error_api));
215
+ }
216
+
217
+ /**
218
+ * Get the PSV node for wanted picture.
219
+ *
220
+ * @param {string} picId The picture ID
221
+ * @returns The PSV node
222
+ * @private
223
+ */
224
+ _getNode(picId) {
225
+ const f = this._sequenceData.find(f => f.properties.id === picId);
226
+ const n = f ? apiFeatureToPSVNode(f, this._t, this._isInternetFast) : null;
227
+ if(n) { delete n.links; }
228
+ return n;
229
+ }
230
+
231
+ /**
232
+ * Creates the widget to switch between aerial and streets imagery
233
+ * @private
234
+ */
235
+ _addMapBackgroundWidget() {
236
+ // Container
237
+ const pnlLayers = document.createElement("div");
238
+ pnlLayers.id = "gvs-map-bg";
239
+ pnlLayers.classList.add("gvs-panel", "gvs-widget-bg", "gvs-input-group");
240
+ const onBgChange = e => this.map.setBackground(e.target.value);
241
+
242
+ // Radio streets
243
+ const radioBgStreets = document.createElement("input");
244
+ radioBgStreets.id = "gvs-map-bg-streets";
245
+ radioBgStreets.setAttribute("type", "radio");
246
+ radioBgStreets.setAttribute("name", "gvs-map-bg");
247
+ radioBgStreets.setAttribute("value", "streets");
248
+ radioBgStreets.addEventListener("change", onBgChange);
249
+ pnlLayers.appendChild(radioBgStreets);
250
+
251
+ const labelBgStreets = document.createElement("label");
252
+ labelBgStreets.setAttribute("for", radioBgStreets.id);
253
+
254
+ const imgBgStreets = document.createElement("img");
255
+ imgBgStreets.src = BackgroundStreets;
256
+
257
+ labelBgStreets.appendChild(imgBgStreets);
258
+ labelBgStreets.appendChild(document.createTextNode(this._t.gvs.map_background_streets));
259
+ pnlLayers.appendChild(labelBgStreets);
260
+
261
+ // Radio aerial
262
+ const radioBgAerial = document.createElement("input");
263
+ radioBgAerial.id = "gvs-map-bg-aerial";
264
+ radioBgAerial.setAttribute("type", "radio");
265
+ radioBgAerial.setAttribute("name", "gvs-map-bg");
266
+ radioBgAerial.setAttribute("value", "aerial");
267
+ radioBgAerial.addEventListener("change", onBgChange);
268
+ pnlLayers.appendChild(radioBgAerial);
269
+
270
+ const labelBgAerial = document.createElement("label");
271
+ labelBgAerial.setAttribute("for", radioBgAerial.id);
272
+
273
+ const imgBgAerial = document.createElement("img");
274
+ imgBgAerial.src = BackgroundAerial;
275
+
276
+ labelBgAerial.appendChild(imgBgAerial);
277
+ labelBgAerial.appendChild(document.createTextNode(this._t.gvs.map_background_aerial));
278
+ pnlLayers.appendChild(labelBgAerial);
279
+
280
+ this.mapContainer.appendChild(pnlLayers);
281
+
282
+ const onMapBgChange = bg => {
283
+ if(bg === "aerial") { radioBgAerial.checked = true; }
284
+ else { radioBgStreets.checked = true; }
285
+ };
286
+ this.addEventListener("map:background-changed", e => onMapBgChange(e.detail.background));
287
+ onMapBgChange(this.map.getBackground());
288
+ }
289
+
290
+ /**
291
+ * Preview on map how the new relative heading would reflect on all pictures.
292
+ * This doesn't change anything on API-side, it's just a preview.
293
+ *
294
+ * @param {number} [relHeading] The new relative heading compared to sequence path. In degrees, between -180 and 180 (0 = front, -90 = left, 90 = right). Set to null to remove preview.
295
+ */
296
+ previewSequenceHeadingChange(relHeading) {
297
+ const layerExists = this.map.getLayer(LAYER_HEADING_ID) !== undefined;
298
+ this.map._picMarkerPreview.remove();
299
+
300
+ // If no value set, remove layer
301
+ if(relHeading === undefined) {
302
+ delete this._lastRelHeading;
303
+ if(layerExists) {
304
+ this.map.setLayoutProperty(LAYER_HEADING_ID, "visibility", "none");
305
+ }
306
+ // Update selected picture marker
307
+ if(this._selectedPicId) {
308
+ this.map._picMarker.setRotation(this.psv.getXY().x);
309
+ }
310
+ return;
311
+ }
312
+
313
+ this._lastRelHeading = relHeading;
314
+
315
+ // Create preview layer
316
+ if(!layerExists) {
317
+ this.map.addLayer({
318
+ "id": LAYER_HEADING_ID,
319
+ "type": "symbol",
320
+ "source": "geovisio_editor_sequences",
321
+ "layout": {
322
+ "icon-image": "gvs-marker",
323
+ "icon-overlap": "always",
324
+ "icon-size": 0.8,
325
+ },
326
+ });
327
+ }
328
+
329
+ // Change heading
330
+ const currentRelHeading = - this.psv.getPictureRelativeHeading();
331
+ this.map.setLayoutProperty(LAYER_HEADING_ID, "visibility", "visible");
332
+ this.map.setLayoutProperty(
333
+ LAYER_HEADING_ID,
334
+ "icon-rotate",
335
+ ["+", ["get", "view:azimuth"], currentRelHeading, relHeading ]
336
+ );
337
+
338
+ // Skip selected picture and linestring geom
339
+ const filters = [["==", ["geometry-type"], "Point"]];
340
+ if(this._selectedPicId) { filters.push(["!=", ["get", "id"], this._selectedPicId]); }
341
+ this.map.setFilter(LAYER_HEADING_ID, ["all", ...filters]);
342
+
343
+ // Update selected picture marker
344
+ if(this._selectedPicId) {
345
+ this.map._picMarker.setRotation(this.psv.getXY().x);
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Event handler for picture loading
351
+ * @private
352
+ */
353
+ _onSelect() {
354
+ // Update preview of heading change
355
+ if(this._lastRelHeading !== undefined) {
356
+ this.previewSequenceHeadingChange(this._lastRelHeading);
357
+ }
358
+ }
359
+ }
@@ -0,0 +1,114 @@
1
+ import CoreView from "./components/CoreView";
2
+ import Map from "./components/Map";
3
+ import { getUserLayerId } from "./utils/Map";
4
+ import { NavigationControl } from "!maplibre-gl"; // DO NOT REMOVE THE "!": bundled builds breaks otherwise !!!
5
+
6
+ /**
7
+ * The standalone map viewer allows to see STAC pictures data as a map.
8
+ * It only embeds a map (no 360° pictures viewer) with a minimal picture preview (thumbnail).
9
+ *
10
+ * Note that you can use any of the [CoreView](#CoreView) class functions as well.
11
+ *
12
+ * @param {string|Element} container The DOM element to create viewer into
13
+ * @param {string} endpoint URL to API to use (must be a [STAC API](https://github.com/radiantearth/stac-api-spec/blob/main/overview.md))
14
+ * @param {object} [options] Map options. 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).
15
+ * @param {string} [options.selectedSequence] The ID of sequence to highlight on load (defaults to none)
16
+ * @param {string} [options.selectedPicture] The ID of picture to highlight on load (defaults to none)
17
+ * @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))
18
+ * @param {number} [options.minZoom=0] The minimum zoom level of the map (0-24).
19
+ * @param {number} [options.maxZoom=24] The maximum zoom level of the map (0-24).
20
+ * @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.
21
+ * @param {external:maplibre-gl.LngLatLike} [options.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.
22
+ * @param {number} [options.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`.
23
+ * @param {external:maplibre-gl.LngLatBoundsLike} [options.bounds] The initial bounds of the map. If `bounds` is specified, it overrides `center` and `zoom` constructor options.
24
+ * @param {string[]} [options.users] The IDs of users whom data should appear on map (defaults to all)
25
+ *
26
+ * @property {Map} map The map widget
27
+ */
28
+ class StandaloneMap extends CoreView {
29
+ constructor(container, endpoint, options = {}) {
30
+ super(container, endpoint, options);
31
+
32
+ this.mapContainer = document.createElement("div");
33
+ this.container.appendChild(this.mapContainer);
34
+
35
+ // Init API
36
+ this._api.onceReady().then(() => this._initMap());
37
+
38
+ // Events handlers
39
+ this.addEventListener("map:picture-click", e => this.select(e.detail.seqId, e.detail.picId));
40
+ this.addEventListener("map:sequence-click", e => this.select(e.detail.seqId));
41
+ this.addEventListener("select", this._onSelect.bind(this));
42
+ }
43
+
44
+ getClassName() {
45
+ return "Map";
46
+ }
47
+
48
+ /**
49
+ * Ends all form of life in this object.
50
+ *
51
+ * This is useful for Single Page Applications (SPA), to remove various event listeners.
52
+ */
53
+ destroy() {
54
+ super.destroy();
55
+
56
+ // Delete sub-components
57
+ this.map.destroy();
58
+ delete this.map;
59
+
60
+ // Clean-up DOM
61
+ this.mapContainer.remove();
62
+ this.container.innerHTML = "";
63
+ this.container.classList.remove(...[...this.container.classList].filter(c => c.startsWith("gvs")));
64
+ }
65
+
66
+ /**
67
+ * Creates map object
68
+ * @private
69
+ */
70
+ _initMap() {
71
+ this._options.hash = true;
72
+
73
+ // Override to avoid display of pictures symbols
74
+ class MyMap extends Map {
75
+ _getLayerStyleProperties(layer) {
76
+ if(layer === "pictures_symbols") {
77
+ return { layout: { visibility: "none" } };
78
+ }
79
+ else {
80
+ return super._getLayerStyleProperties(layer);
81
+ }
82
+ }
83
+ }
84
+
85
+ this.map = new MyMap(this, this.mapContainer, this._options);
86
+ this.map.addControl(new NavigationControl({ showCompass: false }));
87
+ this.map.on("load", () => {
88
+ this.map.reloadLayersStyles();
89
+ this._loader.dismiss();
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Select event handler
95
+ * @private
96
+ * @param {object} e Event details
97
+ */
98
+ _onSelect(e) {
99
+ // Move thumbnail to match selected element
100
+ if(e.detail.picId || e.detail.seqId) {
101
+ const layer = e.detail.picId ? "pictures" : "sequences";
102
+ const features = this.map.queryRenderedFeatures({
103
+ layers: [...this.map._userLayers].map(l => getUserLayerId(l, layer)),
104
+ filter: ["==", ["get", "id"], e.detail.picId || e.detail.seqId]
105
+ });
106
+
107
+ if(features.length >= 0 && features[0] != null) {
108
+ this.map._attachPreviewToPictures({ features }, layer);
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ export default StandaloneMap;
package/src/Viewer.css ADDED
@@ -0,0 +1,203 @@
1
+ /*
2
+ * Sizing of elements
3
+ */
4
+
5
+ /* Focused element */
6
+ .gvs-viewer .gvs-main
7
+ {
8
+ position: relative;
9
+ width: 100%;
10
+ height: 100%;
11
+ z-index: 0;
12
+ }
13
+
14
+ .gvs-viewer .gvs-main {
15
+ display: flex;
16
+ flex-direction: column;
17
+ }
18
+
19
+ /* Non-focused element */
20
+ .gvs-viewer .gvs-mini
21
+ {
22
+ position: absolute;
23
+ top: unset;
24
+ bottom: 10px;
25
+ left: 10px;
26
+ height: 30%;
27
+ min-height: 232px;
28
+ aspect-ratio: 1 / 1;
29
+ z-index: 1;
30
+ }
31
+
32
+ .gvs.gvs-viewer:not(.gvs-has-mini) .gvs-mini,
33
+ .gvs.gvs-viewer.gvs-mini-hidden .gvs-mini {
34
+ display: none;
35
+ }
36
+
37
+ @container (max-width: 576px) { /* Special rule for small containers */
38
+ .gvs-viewer .gvs-mini {
39
+ max-width: 166px;
40
+ max-height: 110px;
41
+ min-height: unset;
42
+ width: 50%;
43
+ height: 30%;
44
+ }
45
+ }
46
+
47
+ .gvs-viewer .gvs-map.maplibregl-map {
48
+ position: absolute;
49
+ top: 0;
50
+ right: 0;
51
+ left: 0;
52
+ bottom: 0;
53
+ }
54
+
55
+ /* PSV fulfilling its parent */
56
+ .gvs-viewer .gvs-psv,
57
+ .gvs-viewer .gvs-popup {
58
+ position: absolute;
59
+ top: 0;
60
+ right: 0;
61
+ left: 0;
62
+ bottom: 0;
63
+ }
64
+
65
+
66
+ /*
67
+ * Styling
68
+ */
69
+
70
+
71
+ /* Non-focused element */
72
+ .gvs-viewer .gvs-mini,
73
+ .gvs-viewer .gvs-mini .psv-container,
74
+ .gvs-viewer .gvs-mini .gvs-map
75
+ {
76
+ border-radius: 10px;
77
+ }
78
+
79
+ /* PSV under widgets */
80
+ .gvs-viewer .psv-container {
81
+ z-index: 0;
82
+ }
83
+
84
+ /* No PSV loader */
85
+ .gvs-viewer .psv-loader {
86
+ display: none;
87
+ }
88
+
89
+ /* Overlay under navbar */
90
+ .gvs-viewer .psv-overlay {
91
+ z-index: 89;
92
+ }
93
+
94
+ /* Popup */
95
+ .gvs-viewer .gvs-popup {
96
+ display: flex;
97
+ justify-content: center;
98
+ align-items: center;
99
+ transition: opacity 0.1s;
100
+ z-index: 10;
101
+ visibility: visible;
102
+ opacity: 1;
103
+ }
104
+
105
+ .gvs-viewer .gvs-popup.gvs-hidden {
106
+ display: flex !important;
107
+ opacity: 0;
108
+ visibility: hidden;
109
+ }
110
+
111
+ .gvs-viewer .gvs-popup-backdrop {
112
+ position: absolute;
113
+ background: rgba(0, 0, 0, 0.85);
114
+ top: 0;
115
+ bottom: 0;
116
+ right: 0;
117
+ left: 0;
118
+ z-index: 10;
119
+ }
120
+
121
+ .gvs-viewer .gvs-popup div.gvs-widget-bg {
122
+ max-width: 90%;
123
+ max-height: 90%;
124
+ position: absolute;
125
+ padding: 15px;
126
+ z-index: 10;
127
+ border-radius: 25px;
128
+ overflow-y: auto;
129
+ }
130
+
131
+ .gvs-viewer #gvs-popup-btn-close {
132
+ position: absolute;
133
+ top: 15px;
134
+ right: 15px;
135
+ width: 24px;
136
+ min-width: unset;
137
+ height: 24px;
138
+ border-radius: 12px;
139
+ }
140
+
141
+ .gvs-viewer .gvs-popup table {
142
+ border-collapse: collapse;
143
+ font-size: 0.9rem;
144
+ width: 100%;
145
+ }
146
+
147
+ .gvs-viewer .gvs-popup thead {
148
+ background-color: var(--blue-pale);
149
+ }
150
+
151
+ .gvs-viewer .gvs-popup th[scope="row"] {
152
+ text-align: left;
153
+ }
154
+
155
+ .gvs-viewer .gvs-popup th, .gvs-popup td {
156
+ border: 1px solid var(--grey-semi-dark);
157
+ padding: 8px 10px;
158
+ max-width: 600px;
159
+ }
160
+
161
+ .gvs-viewer .gvs-popup .gvs-table-light th[scope="row"] {
162
+ width: 30%;
163
+ }
164
+
165
+ .gvs-viewer .gvs-popup .gvs-table-light th,
166
+ .gvs-viewer .gvs-popup .gvs-table-light td {
167
+ border: none;
168
+ padding: 5px 10px;
169
+ text-align: left;
170
+ }
171
+
172
+ .gvs-viewer .gvs-popup .gvs-table-light .gvs-td-with-id {
173
+ display: flex;
174
+ justify-content: space-between;
175
+ align-items: center;
176
+ }
177
+
178
+ .gvs-viewer .gvs-popup table:not(.gvs-table-light) td:last-of-type {
179
+ text-align: center;
180
+ }
181
+
182
+ .gvs-viewer .gvs-popup table:not(.gvs-table-light) tbody > tr:nth-of-type(even) {
183
+ background-color: var(--grey);
184
+ }
185
+
186
+ .gvs-viewer .gvs-popup details summary {
187
+ font-size: 1.0em;
188
+ line-height: 1.0em;
189
+ font-weight: 500;
190
+ margin: 15px 0;
191
+ cursor: pointer;
192
+ }
193
+
194
+ .gvs-viewer .gvs-popup details summary svg {
195
+ height: 18px;
196
+ vertical-align: sub;
197
+ margin-right: 2px;
198
+ }
199
+
200
+ .gvs-viewer .gvs-metadata-actions {
201
+ justify-content: center;
202
+ font-size: 0.9rem;
203
+ }