@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
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
import "./Map.css";
|
|
2
|
+
import {
|
|
3
|
+
forwardGeocodingBAN, forwardGeocodingNominatim, VECTOR_STYLES,
|
|
4
|
+
TILES_PICTURES_ZOOM, getThumbGif, RASTER_LAYER_ID, combineStyles,
|
|
5
|
+
getMissingLayerStyles, isLabelLayer, getUserLayerId, getUserSourceId,
|
|
6
|
+
} from "../utils/Map";
|
|
7
|
+
import { COLORS } from "../utils/Utils";
|
|
8
|
+
import MarkerBaseSVG from "../img/marker.svg";
|
|
9
|
+
import MarkerSelectedSVG from "../img/marker_blue.svg";
|
|
10
|
+
import ArrowFlatSVG from "../img/arrow_flat.svg";
|
|
11
|
+
import Arrow360SVG from "../img/arrow_360.svg";
|
|
12
|
+
|
|
13
|
+
// MapLibre imports
|
|
14
|
+
import "maplibre-gl/dist/maplibre-gl.css";
|
|
15
|
+
import maplibregl from "!maplibre-gl"; // DO NOT REMOVE THE "!": bundled builds breaks otherwise !!!
|
|
16
|
+
import maplibreglWorker from "maplibre-gl/dist/maplibre-gl-csp-worker";
|
|
17
|
+
import * as pmtiles from "pmtiles";
|
|
18
|
+
maplibregl.workerClass = maplibreglWorker;
|
|
19
|
+
maplibregl.addProtocol("pmtiles", new pmtiles.Protocol().tile);
|
|
20
|
+
|
|
21
|
+
const MAPLIBRE_OPTIONS = [ // No "style" option as it's handled by combineStyles function
|
|
22
|
+
"antialias", "attributionControl", "bearing", "bearingSnap", "bounds",
|
|
23
|
+
"boxZoom", "center", "clickTolerance", "collectResourceTiming",
|
|
24
|
+
"cooperativeGestures", "crossSourceCollisions", "doubleClickZoom", "dragPan",
|
|
25
|
+
"dragRotate", "fadeDuration", "failIfMajorPerformanceCaveat", "fitBoundsOptions",
|
|
26
|
+
"hash", "interactive", "keyboard", "localIdeographFontFamily", "locale", "logoPosition",
|
|
27
|
+
"maplibreLogo", "maxBounds", "maxCanvasSize", "maxPitch", "maxTileCacheSize",
|
|
28
|
+
"maxTileCacheZoomLevels", "maxZoom", "minPitch", "minZoom", "pitch", "pitchWithRotate",
|
|
29
|
+
"pixelRatio", "preserveDrawingBuffer", "refreshExpiredTiles", "renderWorldCopies",
|
|
30
|
+
"scrollZoom", "touchPitch", "touchZoomRotate", "trackResize",
|
|
31
|
+
"transformCameraUpdate", "transformRequest", "validateStyle", "zoom"
|
|
32
|
+
];
|
|
33
|
+
const filterMapLibreOptions = opts => Object.fromEntries(Object.entries(opts).filter(([key]) => MAPLIBRE_OPTIONS.includes(key)));
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Map is the component showing pictures and sequences geolocation.
|
|
38
|
+
*
|
|
39
|
+
* Note that all functions of [MapLibre GL JS class Map](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/) are also available.
|
|
40
|
+
*
|
|
41
|
+
* @param {CoreView} parent The parent view
|
|
42
|
+
* @param {Element} container The DOM element to create into
|
|
43
|
+
* @param {object} [options] The map options (any of [MapLibre GL settings](https://maplibre.org/maplibre-gl-js-docs/api/map/#map-parameters) or any supplementary option defined here)
|
|
44
|
+
* @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).
|
|
45
|
+
* @param {string} [options.background] Choose default map background to display (streets or aerial, if raster aerial background available). Defaults to street.
|
|
46
|
+
* @param {object} [options.geocoder] Optional geocoder settings
|
|
47
|
+
* @param {string} [options.geocoder.engine] Set the geocoder engine to use (nominatim, ban)
|
|
48
|
+
*/
|
|
49
|
+
export default class Map extends maplibregl.Map {
|
|
50
|
+
constructor(parent, container, options = {}) {
|
|
51
|
+
super({
|
|
52
|
+
container: container,
|
|
53
|
+
style: combineStyles(parent, options),
|
|
54
|
+
center: [0,0],
|
|
55
|
+
zoom: 0,
|
|
56
|
+
maxZoom: 24,
|
|
57
|
+
attributionControl: false,
|
|
58
|
+
dragRotate: false,
|
|
59
|
+
pitchWithRotate: false,
|
|
60
|
+
preserveDrawingBuffer: !parent.isWidthSmall(),
|
|
61
|
+
transformRequest: parent._api._getMapRequestTransform(),
|
|
62
|
+
locale: parent._t.maplibre,
|
|
63
|
+
...filterMapLibreOptions(options)
|
|
64
|
+
});
|
|
65
|
+
this._loadMarkerImages();
|
|
66
|
+
|
|
67
|
+
this._parent = parent;
|
|
68
|
+
this._options = options;
|
|
69
|
+
this.getContainer().classList.add("gvs-map");
|
|
70
|
+
|
|
71
|
+
// Handle raster source
|
|
72
|
+
if(this._options.raster) {
|
|
73
|
+
this._options.background = this._options.background || "streets";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this._attribution = new maplibregl.AttributionControl({ compact: false });
|
|
77
|
+
this.addControl(this._attribution);
|
|
78
|
+
|
|
79
|
+
this._initGeocoder();
|
|
80
|
+
this._initMapPosition();
|
|
81
|
+
|
|
82
|
+
// Widgets and markers
|
|
83
|
+
this._picMarker = this._getPictureMarker();
|
|
84
|
+
this._picMarkerPreview = this._getPictureMarker(false);
|
|
85
|
+
|
|
86
|
+
// Cache for pictures and sequences thumbnails
|
|
87
|
+
this._picThumbUrl = {};
|
|
88
|
+
this._seqPictures = {};
|
|
89
|
+
|
|
90
|
+
// Sequences and pictures per users
|
|
91
|
+
this._userLayers = new Set();
|
|
92
|
+
|
|
93
|
+
// Hover event
|
|
94
|
+
this.on("mousemove", "sequences", this._onSequenceHover.bind(this));
|
|
95
|
+
|
|
96
|
+
// Parent selection
|
|
97
|
+
this._parent.addEventListener("select", this.reloadLayersStyles.bind(this));
|
|
98
|
+
|
|
99
|
+
this.on("load", async () => {
|
|
100
|
+
await this.setVisibleUsers(this._parent._options.users);
|
|
101
|
+
this.reloadLayersStyles();
|
|
102
|
+
this.resize();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Destroy any form of life in this component
|
|
108
|
+
*/
|
|
109
|
+
destroy() {
|
|
110
|
+
this.remove();
|
|
111
|
+
delete this._parent;
|
|
112
|
+
delete this._options;
|
|
113
|
+
delete this._attribution;
|
|
114
|
+
delete this._picMarker;
|
|
115
|
+
delete this._picMarkerPreview;
|
|
116
|
+
delete this._picThumbUrl;
|
|
117
|
+
delete this._seqPictures;
|
|
118
|
+
delete this._userLayers;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Sets map view based on returned API bbox (if no precise option given by user).
|
|
123
|
+
* @private
|
|
124
|
+
*/
|
|
125
|
+
_initMapPosition() {
|
|
126
|
+
if(
|
|
127
|
+
(!this._options.center || this._options.center == [0,0])
|
|
128
|
+
&& (!this._options.zoom || this._options.zoom === 0)
|
|
129
|
+
&& (!this._options.hash)
|
|
130
|
+
) {
|
|
131
|
+
this._parent._api.onceReady().then(() => {
|
|
132
|
+
let bbox = this._parent?._api?.getDataBbox();
|
|
133
|
+
if(bbox) {
|
|
134
|
+
try {
|
|
135
|
+
bbox = new maplibregl.LngLatBounds(bbox);
|
|
136
|
+
if(this.loaded()) { this.fitBounds(bbox, { "animate": false }); }
|
|
137
|
+
else { this.on("load", () => this.fitBounds(bbox, { "animate": false })); }
|
|
138
|
+
}
|
|
139
|
+
catch(e) {
|
|
140
|
+
console.warn("Received invalid bbox: "+bbox);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Creates the geocoder search bar
|
|
149
|
+
* @private
|
|
150
|
+
*/
|
|
151
|
+
_initGeocoder() {
|
|
152
|
+
const engines = { "ban": forwardGeocodingBAN, "nominatim": forwardGeocodingNominatim };
|
|
153
|
+
const engine = this._options?.geocoder?.engine || "nominatim";
|
|
154
|
+
this.geocoder = engines[engine];
|
|
155
|
+
this._geolocate = new maplibregl.GeolocateControl({
|
|
156
|
+
positionOptions: {
|
|
157
|
+
enableHighAccuracy: true,
|
|
158
|
+
timeout: 60000, // Max 1 minute for first position
|
|
159
|
+
maximumAge: 300000, // Accepts 5 minutes old position
|
|
160
|
+
},
|
|
161
|
+
showAccuracyCircle: true,
|
|
162
|
+
showUserLocation: true,
|
|
163
|
+
trackUserLocation: true,
|
|
164
|
+
}).onAdd(this);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Load markers into map for use in map layers.
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
_loadMarkerImages() {
|
|
172
|
+
[
|
|
173
|
+
{ id: "gvs-marker", img: MarkerBaseSVG },
|
|
174
|
+
{ id: "gvs-arrow-flat", img: ArrowFlatSVG },
|
|
175
|
+
{ id: "gvs-arrow-360", img: Arrow360SVG },
|
|
176
|
+
].forEach(m => {
|
|
177
|
+
const img = new Image(64, 64);
|
|
178
|
+
img.onload = () => this.addImage(m.id, img);
|
|
179
|
+
img.src = m.img;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Force refresh of vector tiles data
|
|
185
|
+
*/
|
|
186
|
+
reloadVectorTiles() {
|
|
187
|
+
[...this._userLayers].forEach(dl => {
|
|
188
|
+
const s = this.getSource(getUserSourceId(dl));
|
|
189
|
+
s.setTiles(s.tiles);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check if map offers aerial imagery as well as streets rendering.
|
|
195
|
+
* @returns {boolean} True if aerial imagery is available for display
|
|
196
|
+
*/
|
|
197
|
+
hasTwoBackgrounds() {
|
|
198
|
+
return this.getLayer(RASTER_LAYER_ID) !== undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get the currently selected map background
|
|
203
|
+
* @returns {string} aerial or streets
|
|
204
|
+
*/
|
|
205
|
+
getBackground() {
|
|
206
|
+
if(!this.getLayer(RASTER_LAYER_ID)) {
|
|
207
|
+
return "streets";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const aerialVisible = this.getLayoutProperty(RASTER_LAYER_ID, "visibility") == "visible";
|
|
211
|
+
return aerialVisible ? "aerial" : "streets";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Change the shown background in map.
|
|
216
|
+
* @param {string} bg The new background to display (aerial or streets)
|
|
217
|
+
*/
|
|
218
|
+
setBackground(bg) {
|
|
219
|
+
if(!this.getLayer(RASTER_LAYER_ID) && bg === "aerial") { throw new Error("No aerial imagery available"); }
|
|
220
|
+
if(this.getLayer(RASTER_LAYER_ID)) {
|
|
221
|
+
this.setLayoutProperty(RASTER_LAYER_ID, "visibility", bg === "aerial" ? "visible" : "none");
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Event for map background changes
|
|
225
|
+
*
|
|
226
|
+
* @event map:background-changed
|
|
227
|
+
* @memberof CoreView
|
|
228
|
+
* @type {object}
|
|
229
|
+
* @property {object} detail Event information
|
|
230
|
+
* @property {string} [detail.background] The new selected background (aerial, streets)
|
|
231
|
+
*/
|
|
232
|
+
const event = new CustomEvent("map:background-changed", { detail: { background: bg || "streets" }});
|
|
233
|
+
this._parent.dispatchEvent(event);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get the currently visible users
|
|
239
|
+
* @returns {string[]} List of visible users
|
|
240
|
+
*/
|
|
241
|
+
getVisibleUsers() {
|
|
242
|
+
return [...this._userLayers].filter(l => (
|
|
243
|
+
this.getLayoutProperty(getUserLayerId(l, "pictures"), "visibility") === "visible"
|
|
244
|
+
));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Make given user layers visible on map, and hide all others (if any)
|
|
249
|
+
*
|
|
250
|
+
* @param {string|string[]} visibleIds The user layers IDs to display
|
|
251
|
+
*/
|
|
252
|
+
async setVisibleUsers(visibleIds = []) {
|
|
253
|
+
if(typeof visibleIds === "string") { visibleIds = [visibleIds]; }
|
|
254
|
+
|
|
255
|
+
// Create any missing user layer
|
|
256
|
+
await Promise.all(
|
|
257
|
+
visibleIds
|
|
258
|
+
.filter(id => id != "" && !this._userLayers.has(id))
|
|
259
|
+
.map(id => this._createPicturesTilesLayer(id))
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Switch visibility
|
|
263
|
+
const layersSuffixes = ["pictures", "sequences", "sequences_plus", "grid", "pictures_symbols"];
|
|
264
|
+
[...this._userLayers].forEach(l => {
|
|
265
|
+
layersSuffixes.forEach(suffix => {
|
|
266
|
+
const layerId = getUserLayerId(l, suffix);
|
|
267
|
+
if(this.getLayer(layerId)) {
|
|
268
|
+
this.setLayoutProperty(layerId, "visibility", visibleIds.includes(l) ? "visible" : "none");
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Event for visible users changes
|
|
275
|
+
*
|
|
276
|
+
* @event map:users-changed
|
|
277
|
+
* @memberof CoreView
|
|
278
|
+
* @type {object}
|
|
279
|
+
* @property {object} detail Event information
|
|
280
|
+
* @property {string[]} [detail.usersIds] The list of newly selected users
|
|
281
|
+
*/
|
|
282
|
+
const event = new CustomEvent("map:users-changed", { detail: { usersIds: visibleIds }});
|
|
283
|
+
this._parent.dispatchEvent(event);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Filter the visible data content in all visible map layers
|
|
288
|
+
* @param {string} dataType sequences or pictures
|
|
289
|
+
* @param {object} filter The MapLibre GL filter rule to apply
|
|
290
|
+
*/
|
|
291
|
+
filterUserLayersContent(dataType, filter) {
|
|
292
|
+
[...this._userLayers].forEach(l => {
|
|
293
|
+
this.setFilter(getUserLayerId(l, dataType), filter);
|
|
294
|
+
if(dataType === "sequences") {
|
|
295
|
+
this.setFilter(getUserLayerId(l, "sequences_plus"), filter);
|
|
296
|
+
}
|
|
297
|
+
if(dataType === "pictures") {
|
|
298
|
+
this.setFilter(getUserLayerId(l, "pictures_symbols"), filter);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Shows on map a picture position and heading.
|
|
305
|
+
*
|
|
306
|
+
* @param {number} lon The longitude
|
|
307
|
+
* @param {number} lat The latitude
|
|
308
|
+
* @param {number} heading The heading
|
|
309
|
+
*/
|
|
310
|
+
displayPictureMarker(lon, lat, heading) {
|
|
311
|
+
this._picMarkerPreview.remove();
|
|
312
|
+
|
|
313
|
+
// Show marker corresponding to selection
|
|
314
|
+
this._picMarker
|
|
315
|
+
.setLngLat([lon, lat])
|
|
316
|
+
.setRotation(heading)
|
|
317
|
+
.addTo(this);
|
|
318
|
+
|
|
319
|
+
// Update map style to see selected sequence
|
|
320
|
+
this.reloadLayersStyles();
|
|
321
|
+
|
|
322
|
+
// Move map to picture coordinates
|
|
323
|
+
this.flyTo({
|
|
324
|
+
center: [lon, lat],
|
|
325
|
+
zoom: this.getZoom() < TILES_PICTURES_ZOOM+2 ? TILES_PICTURES_ZOOM+2 : this.getZoom(),
|
|
326
|
+
maxDuration: 2000
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Forces reload of pictures/sequences layer styles.
|
|
332
|
+
* This is useful after a map theme change.
|
|
333
|
+
*/
|
|
334
|
+
reloadLayersStyles() {
|
|
335
|
+
const updateStyle = (layer, style) => {
|
|
336
|
+
[...this._userLayers].forEach(l => {
|
|
337
|
+
for(let p in style.layout) {
|
|
338
|
+
this.setLayoutProperty(getUserLayerId(l, layer), p, style.layout[p]);
|
|
339
|
+
}
|
|
340
|
+
for(let p in style.paint) {
|
|
341
|
+
this.setPaintProperty(getUserLayerId(l, layer), p, style.paint[p]);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
};
|
|
345
|
+
["pictures", "pictures_symbols", "sequences"].forEach(l => {
|
|
346
|
+
updateStyle(l, this._getLayerStyleProperties(l));
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Creates source and layers for pictures and sequences.
|
|
352
|
+
* @private
|
|
353
|
+
* @param {string} id The source and layer ID prefix
|
|
354
|
+
*/
|
|
355
|
+
async _createPicturesTilesLayer(id) {
|
|
356
|
+
this._userLayers.add(id);
|
|
357
|
+
const firstLabelLayerId = this.getStyle().layers.find(isLabelLayer);
|
|
358
|
+
|
|
359
|
+
// Load style from API
|
|
360
|
+
if(id !== "geovisio" && !this.getSource(`geovisio_${id}`)) {
|
|
361
|
+
const style = await this._parent._api.getUserMapStyle(id);
|
|
362
|
+
Object.entries(style.sources).forEach(([sId, s]) => this.addSource(sId, s));
|
|
363
|
+
style.layers = style.layers || [];
|
|
364
|
+
const layers = style.layers.concat(getMissingLayerStyles(style.sources, style.layers));
|
|
365
|
+
layers.forEach(l => this.addLayer(l, firstLabelLayerId?.id));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Map interaction events
|
|
369
|
+
// Popup
|
|
370
|
+
this._picPreviewTimer = null;
|
|
371
|
+
this._picPopup = new maplibregl.Popup({
|
|
372
|
+
closeButton: false,
|
|
373
|
+
closeOnClick: !this._parent.isWidthSmall(),
|
|
374
|
+
offset: 3
|
|
375
|
+
});
|
|
376
|
+
this._picPopup.on("close", () => { delete this._picPopup._picId; });
|
|
377
|
+
|
|
378
|
+
// Pictures
|
|
379
|
+
const picLayerId = getUserLayerId(id, "pictures");
|
|
380
|
+
this.on("mousemove", picLayerId, e => {
|
|
381
|
+
this.getCanvas().style.cursor = "pointer";
|
|
382
|
+
const eCopy = Object.assign({}, e);
|
|
383
|
+
clearTimeout(this._picPreviewTimer);
|
|
384
|
+
this._picPreviewTimer = setTimeout(
|
|
385
|
+
() => this._attachPreviewToPictures(eCopy, picLayerId),
|
|
386
|
+
100
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
this.on("mouseleave", picLayerId, () => {
|
|
391
|
+
clearTimeout(this._picPreviewTimer);
|
|
392
|
+
this.getCanvas().style.cursor = "";
|
|
393
|
+
this._picPopup._picId;
|
|
394
|
+
this._picPopup.remove();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
this.on("click", picLayerId, this._onPictureClick.bind(this));
|
|
398
|
+
|
|
399
|
+
// Sequences
|
|
400
|
+
const seqPlusLayerId = getUserLayerId(id, "sequences_plus");
|
|
401
|
+
this.on("mousemove", seqPlusLayerId, e => {
|
|
402
|
+
if(this.getZoom() <= TILES_PICTURES_ZOOM+1) {
|
|
403
|
+
this.getCanvas().style.cursor = "pointer";
|
|
404
|
+
if(e.features[0].properties.id) {
|
|
405
|
+
const eCopy = Object.assign({}, e);
|
|
406
|
+
clearTimeout(this._picPreviewTimer);
|
|
407
|
+
this._picPreviewTimer = setTimeout(
|
|
408
|
+
() => this._attachPreviewToPictures(eCopy, seqPlusLayerId),
|
|
409
|
+
100
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
this.on("mouseleave", seqPlusLayerId, () => {
|
|
416
|
+
clearTimeout(this._picPreviewTimer);
|
|
417
|
+
this.getCanvas().style.cursor = "";
|
|
418
|
+
this._picPopup._picId;
|
|
419
|
+
this._picPopup.remove();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
this.on("click", seqPlusLayerId, e => {
|
|
423
|
+
e.preventDefault();
|
|
424
|
+
if(this.getZoom() <= TILES_PICTURES_ZOOM+1) {
|
|
425
|
+
this._onSequenceClick(e);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Grid
|
|
430
|
+
if(id === "geovisio" && this.getLayer("geovisio_grid")) {
|
|
431
|
+
this.on("mousemove", "geovisio_grid", e => {
|
|
432
|
+
if(this.getZoom() <= TILES_PICTURES_ZOOM+1) {
|
|
433
|
+
this.getCanvas().style.cursor = "pointer";
|
|
434
|
+
const eCopy = Object.assign({}, e);
|
|
435
|
+
clearTimeout(this._picPreviewTimer);
|
|
436
|
+
this._picPreviewTimer = setTimeout(
|
|
437
|
+
() => this._attachPreviewToPictures(eCopy, "geovisio_grid"),
|
|
438
|
+
100
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
this.on("mouseleave", "geovisio_grid", () => {
|
|
444
|
+
clearTimeout(this._picPreviewTimer);
|
|
445
|
+
this.getCanvas().style.cursor = "";
|
|
446
|
+
this._picPopup._picId;
|
|
447
|
+
this._picPopup.remove();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
this.on("click", "geovisio_grid", e => {
|
|
451
|
+
e.preventDefault();
|
|
452
|
+
this.flyTo({ center: e.lngLat, zoom: TILES_PICTURES_ZOOM-6 });
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Map background click
|
|
457
|
+
this.on("click", (e) => {
|
|
458
|
+
if(e.defaultPrevented === false) {
|
|
459
|
+
clearTimeout(this._picPreviewTimer);
|
|
460
|
+
this._picPopup.remove();
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* MapLibre paint/layout properties for specific layer
|
|
467
|
+
* This is useful when selected picture changes to allow partial update
|
|
468
|
+
*
|
|
469
|
+
* @returns {object} Paint/layout properties
|
|
470
|
+
* @private
|
|
471
|
+
*/
|
|
472
|
+
_getLayerStyleProperties(layer) {
|
|
473
|
+
if(layer === "pictures_symbols") {
|
|
474
|
+
return {
|
|
475
|
+
"paint": {},
|
|
476
|
+
"layout": {
|
|
477
|
+
"icon-image": ["case",
|
|
478
|
+
["==", ["get", "id"], this._parent._selectedPicId], "",
|
|
479
|
+
["==", ["get", "type"], "equirectangular"], "gvs-arrow-360",
|
|
480
|
+
"gvs-arrow-flat"
|
|
481
|
+
],
|
|
482
|
+
"symbol-sort-key": this._getLayerSortStyle(layer),
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
const prefixes = {
|
|
488
|
+
"pictures": "circle",
|
|
489
|
+
"sequences": "line",
|
|
490
|
+
};
|
|
491
|
+
return {
|
|
492
|
+
"paint": Object.assign({
|
|
493
|
+
[`${prefixes[layer]}-color`]: this._getLayerColorStyle(layer),
|
|
494
|
+
}, VECTOR_STYLES[layer.toUpperCase()].paint),
|
|
495
|
+
"layout": Object.assign({
|
|
496
|
+
[`${prefixes[layer]}-sort-key`]: this._getLayerSortStyle(layer),
|
|
497
|
+
}, VECTOR_STYLES[layer.toUpperCase()].layout)
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Retrieve map layer color scheme according to selected theme.
|
|
504
|
+
* @private
|
|
505
|
+
*/
|
|
506
|
+
_getLayerColorStyle(layer) {
|
|
507
|
+
// Hidden style
|
|
508
|
+
const s = ["case",
|
|
509
|
+
["==", ["get", "hidden"], true], COLORS.HIDDEN,
|
|
510
|
+
["==", ["get", "geovisio:status"], "hidden"], COLORS.HIDDEN,
|
|
511
|
+
];
|
|
512
|
+
|
|
513
|
+
// Selected sequence style
|
|
514
|
+
const seqId = this._parent._selectedSeqId;
|
|
515
|
+
if(layer == "sequences" && seqId) {
|
|
516
|
+
s.push(["==", ["get", "id"], seqId], COLORS.SELECTED);
|
|
517
|
+
}
|
|
518
|
+
else if(layer.startsWith("pictures") && seqId) {
|
|
519
|
+
s.push(["in", seqId, ["get", "sequences"]], COLORS.SELECTED);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Classic style
|
|
523
|
+
s.push(COLORS.BASE);
|
|
524
|
+
|
|
525
|
+
return s;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Retrieve map sort key according to selected theme.
|
|
530
|
+
* @private
|
|
531
|
+
*/
|
|
532
|
+
_getLayerSortStyle(layer) {
|
|
533
|
+
// Values
|
|
534
|
+
// - 100 : on top / selected feature
|
|
535
|
+
// - 90 : hidden feature
|
|
536
|
+
// - 20-80 : custom ranges
|
|
537
|
+
// - 10 : basic feature
|
|
538
|
+
// - 0 : on bottom / feature with undefined property
|
|
539
|
+
// Hidden style
|
|
540
|
+
const s = ["case",
|
|
541
|
+
["==", ["get", "hidden"], true], 90
|
|
542
|
+
];
|
|
543
|
+
|
|
544
|
+
// Selected sequence style
|
|
545
|
+
const seqId = this._parent._selectedSeqId;
|
|
546
|
+
if(layer == "sequences" && seqId) {
|
|
547
|
+
s.push(["==", ["get", "id"], seqId], 100);
|
|
548
|
+
}
|
|
549
|
+
else if(layer.startsWith("pictures") && seqId) {
|
|
550
|
+
s.push(["in", seqId, ["get", "sequences"]], 100);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
s.push(10);
|
|
554
|
+
return s;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Creates popup manager for preview of pictures.
|
|
559
|
+
* @private
|
|
560
|
+
* @param {object} e The event thrown by MapLibre
|
|
561
|
+
* @param {string} from The event source layer
|
|
562
|
+
*/
|
|
563
|
+
_attachPreviewToPictures(e, from) {
|
|
564
|
+
let f = e.features.pop();
|
|
565
|
+
if(!f || f.properties.id == this._picPopup._picId) { return; }
|
|
566
|
+
|
|
567
|
+
let coordinates = null;
|
|
568
|
+
if(from.endsWith("pictures")) { coordinates = f.geometry.coordinates.slice(); }
|
|
569
|
+
else if(e.lngLat) { coordinates = [e.lngLat.lng, e.lngLat.lat]; }
|
|
570
|
+
|
|
571
|
+
// If no coordinates found, find from geometry (nearest to map center)
|
|
572
|
+
if(!coordinates) {
|
|
573
|
+
const coords = f.geometry.type === "LineString" ? [f.geometry.coordinates] : f.geometry.coordinates;
|
|
574
|
+
let prevDist = null;
|
|
575
|
+
const mapBbox = this.getBounds();
|
|
576
|
+
const mapCenter = mapBbox.getCenter();
|
|
577
|
+
for(let i=0; i < coords.length; i++) {
|
|
578
|
+
for(let j=0; j < coords[i].length; j++) {
|
|
579
|
+
if(mapBbox.contains(coords[i][j])) {
|
|
580
|
+
let dist = mapCenter.distanceTo(new maplibregl.LngLat(...coords[i][j]));
|
|
581
|
+
if(prevDist === null || dist < prevDist) {
|
|
582
|
+
coordinates = coords[i][j];
|
|
583
|
+
prevDist = dist;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if(!coordinates) { return; }
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Display thumbnail
|
|
593
|
+
this._picPopup
|
|
594
|
+
.setLngLat(coordinates)
|
|
595
|
+
.addTo(this);
|
|
596
|
+
|
|
597
|
+
// Only show GIF loader if thumbnail is not in browser cache
|
|
598
|
+
if(!this._picThumbUrl[f.properties.id]) {
|
|
599
|
+
this._picPopup.setDOMContent(getThumbGif(this._parent._t));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
this._picPopup._loading = f.properties.id;
|
|
603
|
+
this._picPopup._picId = f.properties.id;
|
|
604
|
+
|
|
605
|
+
const displayThumb = thumbUrl => {
|
|
606
|
+
if(this._picPopup._loading === f.properties.id) {
|
|
607
|
+
delete this._picPopup._loading;
|
|
608
|
+
|
|
609
|
+
if(thumbUrl) {
|
|
610
|
+
let content = document.createElement("img");
|
|
611
|
+
content.classList.add("gvs-map-thumb");
|
|
612
|
+
content.alt = this._parent._t.thumbnail;
|
|
613
|
+
let img = new Image();
|
|
614
|
+
img.src = thumbUrl;
|
|
615
|
+
|
|
616
|
+
img.addEventListener("load", () => {
|
|
617
|
+
if(f.properties.hidden) {
|
|
618
|
+
content.children[0].src = img.src;
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
content.src = img.src;
|
|
622
|
+
}
|
|
623
|
+
this._picPopup.setDOMContent(content);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
if(f.properties.hidden) {
|
|
627
|
+
const legend = document.createElement("div");
|
|
628
|
+
legend.classList.add("gvs-map-thumb-legend");
|
|
629
|
+
legend.appendChild(document.createTextNode(this._parent._t.map.not_public));
|
|
630
|
+
const container = document.createElement("div");
|
|
631
|
+
container.appendChild(content);
|
|
632
|
+
container.appendChild(legend);
|
|
633
|
+
content = container;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
this._picPopup.setHTML(`<i>${this._parent._t.no_thumbnail}</i>`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// Click on a single picture
|
|
643
|
+
if(from.endsWith("pictures")) {
|
|
644
|
+
this._getPictureThumbURL(f.properties.id).then(displayThumb);
|
|
645
|
+
}
|
|
646
|
+
// Click on a grid cell
|
|
647
|
+
else if(from.endsWith("grid")) {
|
|
648
|
+
this._getThumbURL(coordinates).then(displayThumb);
|
|
649
|
+
}
|
|
650
|
+
// Click on a sequence
|
|
651
|
+
else {
|
|
652
|
+
this._getSequenceThumbURL(f.properties.id, new maplibregl.LngLat(...coordinates)).then(displayThumb);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Get picture thumbnail URL at given coordinates
|
|
658
|
+
*
|
|
659
|
+
* @param {LngLat} coordinates The map coordinates
|
|
660
|
+
* @returns {Promise} Promise resolving on picture thumbnail URL, or null on timeout
|
|
661
|
+
* @private
|
|
662
|
+
*/
|
|
663
|
+
_getThumbURL(coordinates) {
|
|
664
|
+
return this._parent._api.getPicturesAroundCoordinates(coordinates[1], coordinates[0], 0.1, 1).then(res => {
|
|
665
|
+
const p = res?.features?.pop();
|
|
666
|
+
return p ? this._parent._api.findThumbnailInPictureFeature(p) : null;
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Get picture thumbnail URL for a given sequence ID
|
|
673
|
+
*
|
|
674
|
+
* @param {string} seqId The sequence ID
|
|
675
|
+
* @param {LngLat} [coordinates] The map coordinates
|
|
676
|
+
* @returns {Promise} Promise resolving on picture thumbnail URL, or null on timeout
|
|
677
|
+
* @private
|
|
678
|
+
*/
|
|
679
|
+
_getSequenceThumbURL(seqId, coordinates) {
|
|
680
|
+
if(coordinates) {
|
|
681
|
+
return this._parent._api.getPicturesAroundCoordinates(coordinates.lat, coordinates.lng, 1, 1, seqId)
|
|
682
|
+
.then(results => {
|
|
683
|
+
if(results?.features?.length > 0) {
|
|
684
|
+
return this._parent._api.findThumbnailInPictureFeature(results.features[0]);
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
return this._parent._api.getPictureThumbnailURLForSequence(seqId);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
return this._parent._api.getPictureThumbnailURLForSequence(seqId);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Get picture thumbnail URL for a given picture ID.
|
|
698
|
+
* It handles a client-side cache based on raw API responses.
|
|
699
|
+
*
|
|
700
|
+
* @param {string} picId The picture ID
|
|
701
|
+
* @param {string} [seqId] The sequence ID (can speed up search if available)
|
|
702
|
+
* @returns {Promise} Promise resolving on picture thumbnail URL, or null on timeout
|
|
703
|
+
*
|
|
704
|
+
* @private
|
|
705
|
+
*/
|
|
706
|
+
_getPictureThumbURL(picId, seqId) {
|
|
707
|
+
let res = null;
|
|
708
|
+
|
|
709
|
+
if(picId) {
|
|
710
|
+
if(this._picThumbUrl[picId] !== undefined) {
|
|
711
|
+
res = typeof this._picThumbUrl[picId] === "string" ? Promise.resolve(this._picThumbUrl[picId]) : this._picThumbUrl[picId];
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
this._picThumbUrl[picId] = this._parent._api.getPictureThumbnailURL(picId, seqId).then(url => {
|
|
715
|
+
if(url) {
|
|
716
|
+
this._picThumbUrl[picId] = url;
|
|
717
|
+
return url;
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
this._picThumbUrl[picId] = null;
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
})
|
|
724
|
+
.catch(() => {
|
|
725
|
+
this._picThumbUrl[picId] = null;
|
|
726
|
+
});
|
|
727
|
+
res = this._picThumbUrl[picId];
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return res;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Create a ready-to-use picture marker
|
|
736
|
+
*
|
|
737
|
+
* @returns {maplibregl.Marker} The generated marker
|
|
738
|
+
* @private
|
|
739
|
+
*/
|
|
740
|
+
_getPictureMarker(selected = true) {
|
|
741
|
+
const img = document.createElement("img");
|
|
742
|
+
img.src = selected ? MarkerSelectedSVG : MarkerBaseSVG;
|
|
743
|
+
return new maplibregl.Marker({
|
|
744
|
+
element: img
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Event handler for sequence hover
|
|
750
|
+
* @private
|
|
751
|
+
* @param {object} e Event data
|
|
752
|
+
*/
|
|
753
|
+
_onSequenceHover(e) {
|
|
754
|
+
e.preventDefault();
|
|
755
|
+
if(e.features.length > 0 && e.features[0].properties?.id) {
|
|
756
|
+
/**
|
|
757
|
+
* Event when a sequence on map is hovered (not selected)
|
|
758
|
+
*
|
|
759
|
+
* @event map:sequence-hover
|
|
760
|
+
* @memberof CoreView
|
|
761
|
+
* @type {object}
|
|
762
|
+
* @property {object} detail Event information
|
|
763
|
+
* @property {string} detail.seqId The hovered sequence ID
|
|
764
|
+
*/
|
|
765
|
+
this._parent.dispatchEvent(new CustomEvent("map:sequence-hover", {
|
|
766
|
+
detail: {
|
|
767
|
+
seqId: e.features[0].properties.id
|
|
768
|
+
}
|
|
769
|
+
}));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Event handler for sequence click
|
|
775
|
+
* @private
|
|
776
|
+
* @param {object} e Event data
|
|
777
|
+
*/
|
|
778
|
+
_onSequenceClick(e) {
|
|
779
|
+
e.preventDefault();
|
|
780
|
+
if(e.features.length > 0 && e.features[0].properties?.id) {
|
|
781
|
+
/**
|
|
782
|
+
* Event when a sequence on map is clicked
|
|
783
|
+
*
|
|
784
|
+
* @event map:sequence-click
|
|
785
|
+
* @memberof CoreView
|
|
786
|
+
* @type {object}
|
|
787
|
+
* @property {object} detail Event information
|
|
788
|
+
* @property {string} detail.seqId The clicked sequence ID
|
|
789
|
+
* @property {maplibregl.LngLat} detail.coordinates The coordinates of user click
|
|
790
|
+
*/
|
|
791
|
+
this._parent.dispatchEvent(new CustomEvent("map:sequence-click", {
|
|
792
|
+
detail: {
|
|
793
|
+
seqId: e.features[0].properties.id,
|
|
794
|
+
coordinates: e.lngLat
|
|
795
|
+
}
|
|
796
|
+
}));
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Event handler for picture click
|
|
802
|
+
* @private
|
|
803
|
+
* @param {object} e Event data
|
|
804
|
+
*/
|
|
805
|
+
_onPictureClick(e) {
|
|
806
|
+
e.preventDefault();
|
|
807
|
+
const f = e?.features?.length > 0 ? e.features[0] : null;
|
|
808
|
+
if(f?.properties?.id) {
|
|
809
|
+
// Look for a potential sequence ID
|
|
810
|
+
let seqId = null;
|
|
811
|
+
try {
|
|
812
|
+
if(f.properties.sequences) {
|
|
813
|
+
if(!Array.isArray(f.properties.sequences)) { f.properties.sequences = JSON.parse(f.properties.sequences); }
|
|
814
|
+
seqId = f.properties.sequences.pop();
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
catch(e) {
|
|
818
|
+
console.log("Sequence ID is not available in vector tiles for picture "+f.properties.id);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Event when a picture on map is clicked
|
|
823
|
+
*
|
|
824
|
+
* @event map:picture-click
|
|
825
|
+
* @memberof CoreView
|
|
826
|
+
* @type {object}
|
|
827
|
+
* @property {object} detail Event information
|
|
828
|
+
* @property {string} detail.picId The clicked picture ID
|
|
829
|
+
* @property {string} detail.seqId The clicked picture's sequence ID
|
|
830
|
+
* @property {object} detail.feature The GeoJSON feature of the picture
|
|
831
|
+
*/
|
|
832
|
+
this._parent.dispatchEvent(new CustomEvent("map:picture-click", {
|
|
833
|
+
detail: {
|
|
834
|
+
picId: f.properties.id,
|
|
835
|
+
seqId,
|
|
836
|
+
feature: f
|
|
837
|
+
}
|
|
838
|
+
}));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|