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