@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.
- package/.dockerignore +6 -0
- package/.gitlab-ci.yml +71 -0
- package/CHANGELOG.md +428 -0
- package/CODE_OF_CONDUCT.md +134 -0
- package/Dockerfile +14 -0
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/build/editor.html +1 -0
- package/build/index.css +36 -0
- package/build/index.css.map +1 -0
- package/build/index.html +1 -0
- package/build/index.js +25 -0
- package/build/index.js.map +1 -0
- package/build/map.html +1 -0
- package/build/viewer.html +1 -0
- package/config/env.js +104 -0
- package/config/getHttpsConfig.js +66 -0
- package/config/getPackageJson.js +25 -0
- package/config/jest/babelTransform.js +29 -0
- package/config/jest/cssTransform.js +14 -0
- package/config/jest/fileTransform.js +40 -0
- package/config/modules.js +134 -0
- package/config/paths.js +72 -0
- package/config/pnpTs.js +35 -0
- package/config/webpack/persistentCache/createEnvironmentHash.js +9 -0
- package/config/webpack.config.js +885 -0
- package/config/webpackDevServer.config.js +127 -0
- package/docs/01_Start.md +149 -0
- package/docs/02_Usage.md +828 -0
- package/docs/03_URL_settings.md +140 -0
- package/docs/04_Advanced_examples.md +214 -0
- package/docs/05_Compatibility.md +85 -0
- package/docs/09_Develop.md +62 -0
- package/docs/90_Releases.md +27 -0
- package/docs/images/class_diagram.drawio +129 -0
- package/docs/images/class_diagram.jpg +0 -0
- package/docs/images/screenshot.jpg +0 -0
- package/mkdocs.yml +45 -0
- package/package.json +254 -0
- package/public/editor.html +54 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +59 -0
- package/public/map.html +53 -0
- package/public/viewer.html +67 -0
- package/scripts/build.js +217 -0
- package/scripts/start.js +176 -0
- package/scripts/test.js +52 -0
- package/src/Editor.css +37 -0
- package/src/Editor.js +359 -0
- package/src/StandaloneMap.js +114 -0
- package/src/Viewer.css +203 -0
- package/src/Viewer.js +1186 -0
- package/src/components/CoreView.css +64 -0
- package/src/components/CoreView.js +159 -0
- package/src/components/Loader.css +56 -0
- package/src/components/Loader.js +111 -0
- package/src/components/Map.css +65 -0
- package/src/components/Map.js +841 -0
- package/src/components/Photo.css +36 -0
- package/src/components/Photo.js +687 -0
- package/src/img/arrow_360.svg +14 -0
- package/src/img/arrow_flat.svg +11 -0
- package/src/img/arrow_triangle.svg +10 -0
- package/src/img/arrow_turn.svg +9 -0
- package/src/img/bg_aerial.jpg +0 -0
- package/src/img/bg_streets.jpg +0 -0
- package/src/img/loader_base.jpg +0 -0
- package/src/img/loader_hd.jpg +0 -0
- package/src/img/logo_dead.svg +91 -0
- package/src/img/marker.svg +17 -0
- package/src/img/marker_blue.svg +20 -0
- package/src/img/switch_big.svg +44 -0
- package/src/img/switch_mini.svg +48 -0
- package/src/index.js +10 -0
- package/src/translations/de.json +163 -0
- package/src/translations/en.json +164 -0
- package/src/translations/eo.json +6 -0
- package/src/translations/es.json +164 -0
- package/src/translations/fi.json +1 -0
- package/src/translations/fr.json +164 -0
- package/src/translations/hu.json +133 -0
- package/src/translations/nl.json +1 -0
- package/src/translations/zh_Hant.json +136 -0
- package/src/utils/API.js +709 -0
- package/src/utils/Exif.js +198 -0
- package/src/utils/I18n.js +75 -0
- package/src/utils/Map.js +382 -0
- package/src/utils/PhotoAdapter.js +45 -0
- package/src/utils/Utils.js +568 -0
- package/src/utils/Widgets.js +477 -0
- package/src/viewer/URLHash.js +334 -0
- package/src/viewer/Widgets.css +711 -0
- package/src/viewer/Widgets.js +1196 -0
- package/tests/Editor.test.js +125 -0
- package/tests/StandaloneMap.test.js +44 -0
- package/tests/Viewer.test.js +363 -0
- package/tests/__snapshots__/Editor.test.js.snap +300 -0
- package/tests/__snapshots__/StandaloneMap.test.js.snap +30 -0
- package/tests/__snapshots__/Viewer.test.js.snap +195 -0
- package/tests/components/CoreView.test.js +91 -0
- package/tests/components/Loader.test.js +38 -0
- package/tests/components/Map.test.js +230 -0
- package/tests/components/Photo.test.js +335 -0
- package/tests/components/__snapshots__/Loader.test.js.snap +15 -0
- package/tests/components/__snapshots__/Map.test.js.snap +767 -0
- package/tests/components/__snapshots__/Photo.test.js.snap +205 -0
- package/tests/data/Map_geocoder_ban.json +36 -0
- package/tests/data/Map_geocoder_nominatim.json +56 -0
- package/tests/data/Viewer_pictures_1.json +148 -0
- package/tests/setupTests.js +5 -0
- package/tests/utils/API.test.js +906 -0
- package/tests/utils/Exif.test.js +124 -0
- package/tests/utils/I18n.test.js +28 -0
- package/tests/utils/Map.test.js +105 -0
- package/tests/utils/Utils.test.js +300 -0
- package/tests/utils/Widgets.test.js +107 -0
- package/tests/utils/__snapshots__/API.test.js.snap +132 -0
- package/tests/utils/__snapshots__/Exif.test.js.snap +43 -0
- package/tests/utils/__snapshots__/Map.test.js.snap +48 -0
- package/tests/utils/__snapshots__/Utils.test.js.snap +41 -0
- package/tests/utils/__snapshots__/Widgets.test.js.snap +44 -0
- package/tests/viewer/URLHash.test.js +537 -0
- package/tests/viewer/Widgets.test.js +127 -0
- package/tests/viewer/__snapshots__/URLHash.test.js.snap +98 -0
- 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
|
+
}
|