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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/build/bundle.cjs +3399 -0
  2. package/build/bundle.cjs.map +1 -0
  3. package/build/bundle_photoviewer.cjs +2510 -0
  4. package/build/bundle_photoviewer.cjs.map +1 -0
  5. package/build/cjs/index.js +1 -1
  6. package/build/cjs/index_photoviewer.js +1 -1
  7. package/build/components/core/Basic.css +56 -0
  8. package/build/components/core/Basic.js +378 -0
  9. package/build/components/core/CoverageMap.css +10 -0
  10. package/build/components/core/CoverageMap.js +169 -0
  11. package/build/components/core/Editor.css +33 -0
  12. package/build/components/core/Editor.js +398 -0
  13. package/build/components/core/PhotoViewer.css +70 -0
  14. package/build/components/core/PhotoViewer.js +650 -0
  15. package/build/components/core/Viewer.css +130 -0
  16. package/build/components/core/Viewer.js +711 -0
  17. package/build/components/core/index.js +10 -0
  18. package/build/components/index.js +11 -0
  19. package/build/components/index_photoviewer.js +6 -0
  20. package/build/components/layout/BottomDrawer.js +258 -0
  21. package/build/components/layout/CorneredGrid.js +143 -0
  22. package/build/components/layout/Mini.js +121 -0
  23. package/build/components/layout/Tabs.js +140 -0
  24. package/build/components/layout/index.js +9 -0
  25. package/build/components/menus/LocationPrecisionDoc.js +42 -0
  26. package/build/components/menus/MapBackground.js +110 -0
  27. package/build/components/menus/MapFilters.js +567 -0
  28. package/build/components/menus/MapLayers.js +238 -0
  29. package/build/components/menus/MapLegend.js +68 -0
  30. package/build/components/menus/MiniPictureLegend.js +73 -0
  31. package/build/components/menus/PictureLegend.js +379 -0
  32. package/build/components/menus/PictureMetadata.js +380 -0
  33. package/build/components/menus/PlayerOptions.js +93 -0
  34. package/build/components/menus/QualityScoreDoc.js +42 -0
  35. package/build/components/menus/ReportForm.js +132 -0
  36. package/build/components/menus/SemanticsDoc.js +38 -0
  37. package/build/components/menus/SemanticsDownload.js +33 -0
  38. package/build/components/menus/SemanticsFilters.js +153 -0
  39. package/build/components/menus/SemanticsList.js +413 -0
  40. package/build/components/menus/SemanticsMetadata.js +368 -0
  41. package/build/components/menus/Share.js +105 -0
  42. package/build/components/menus/index.js +22 -0
  43. package/build/components/menus/index_photoviewer.js +11 -0
  44. package/build/components/styles.js +557 -0
  45. package/build/components/ui/AnnotationsSwitch.js +159 -0
  46. package/build/components/ui/Button.js +77 -0
  47. package/build/components/ui/ButtonGroup.css +59 -0
  48. package/build/components/ui/ButtonGroup.js +69 -0
  49. package/build/components/ui/CopyButton.js +110 -0
  50. package/build/components/ui/Grade.js +54 -0
  51. package/build/components/ui/GradeFilter.js +122 -0
  52. package/build/components/ui/IconSwitch.js +193 -0
  53. package/build/components/ui/LinkButton.js +67 -0
  54. package/build/components/ui/ListGroup.js +66 -0
  55. package/build/components/ui/ListItem.js +90 -0
  56. package/build/components/ui/Loader.js +203 -0
  57. package/build/components/ui/Map.css +63 -0
  58. package/build/components/ui/Map.js +853 -0
  59. package/build/components/ui/MapMore.js +175 -0
  60. package/build/components/ui/Photo.css +50 -0
  61. package/build/components/ui/Photo.js +1502 -0
  62. package/build/components/ui/Popup.js +145 -0
  63. package/build/components/ui/ProgressBar.js +104 -0
  64. package/build/components/ui/QualityScore.js +147 -0
  65. package/build/components/ui/SearchBar.js +374 -0
  66. package/build/components/ui/SemanticsEditor.js +191 -0
  67. package/build/components/ui/SemanticsTable.js +88 -0
  68. package/build/components/ui/Switch.js +139 -0
  69. package/build/components/ui/TogglableGroup.js +157 -0
  70. package/build/components/ui/index.js +29 -0
  71. package/build/components/ui/index_photoviewer.js +21 -0
  72. package/build/components/ui/widgets/CopyCoordinates.js +75 -0
  73. package/build/components/ui/widgets/GeoSearch.css +21 -0
  74. package/build/components/ui/widgets/GeoSearch.js +150 -0
  75. package/build/components/ui/widgets/Legend.js +190 -0
  76. package/build/components/ui/widgets/LevelSelect.css +51 -0
  77. package/build/components/ui/widgets/LevelSelect.js +143 -0
  78. package/build/components/ui/widgets/MapFiltersButton.js +114 -0
  79. package/build/components/ui/widgets/MapLayersButton.js +79 -0
  80. package/build/components/ui/widgets/OSMEditors.js +155 -0
  81. package/build/components/ui/widgets/PictureLegendActions.js +99 -0
  82. package/build/components/ui/widgets/Player.css +7 -0
  83. package/build/components/ui/widgets/Player.js +154 -0
  84. package/build/components/ui/widgets/SemanticsFiltersButton.js +65 -0
  85. package/build/components/ui/widgets/Zoom.js +84 -0
  86. package/build/components/ui/widgets/index.js +16 -0
  87. package/build/components/ui/widgets/index_photoviewer.js +7 -0
  88. package/build/esm/components/core/Basic.js +1 -1
  89. package/build/img/arrow_360.svg +14 -0
  90. package/build/img/arrow_flat.svg +11 -0
  91. package/build/img/arrow_triangle.svg +9 -0
  92. package/build/img/arrow_turn.svg +8 -0
  93. package/build/img/bg_aerial.jpg +0 -0
  94. package/build/img/bg_streets.jpg +0 -0
  95. package/build/img/loader_base.jpg +0 -0
  96. package/build/img/logo_dead.svg +91 -0
  97. package/build/img/marker.svg +17 -0
  98. package/build/img/marker_blue.svg +20 -0
  99. package/build/img/osm.svg +49 -0
  100. package/build/img/panoramax.svg +13 -0
  101. package/build/img/switch_big.svg +54 -0
  102. package/build/img/switch_mini.svg +48 -0
  103. package/build/img/wd.svg +1 -0
  104. package/build/index_photoviewer.js +4 -0
  105. package/build/package.json +148 -0
  106. package/build/servers.js +14 -0
  107. package/build/translations/ar.json +1 -0
  108. package/build/translations/be.json +257 -0
  109. package/build/translations/br.json +81 -0
  110. package/build/translations/cy.json +117 -0
  111. package/build/translations/da.json +300 -0
  112. package/build/translations/de.json +309 -0
  113. package/build/translations/en.json +294 -0
  114. package/build/translations/eo.json +235 -0
  115. package/build/translations/es.json +292 -0
  116. package/build/translations/fi.json +1 -0
  117. package/build/translations/fr.json +294 -0
  118. package/build/translations/hr.json +294 -0
  119. package/build/translations/hu.json +294 -0
  120. package/build/translations/it.json +306 -0
  121. package/build/translations/ja.json +182 -0
  122. package/build/translations/ko.json +1 -0
  123. package/build/translations/nl.json +305 -0
  124. package/build/translations/nn.json +1 -0
  125. package/build/translations/pl.json +169 -0
  126. package/build/translations/pt.json +296 -0
  127. package/build/translations/pt_BR.json +304 -0
  128. package/build/translations/sv.json +182 -0
  129. package/build/translations/ti.json +9 -0
  130. package/build/translations/tr.json +297 -0
  131. package/build/translations/uk.json +268 -0
  132. package/build/translations/zh_Hant.json +309 -0
  133. package/build/utils/API.js +928 -0
  134. package/build/utils/InitParameters.js +521 -0
  135. package/build/utils/MapStyleComposer.js +889 -0
  136. package/build/utils/PanoraMapProtocol.js +49 -0
  137. package/build/utils/PhotoAdapter.js +49 -0
  138. package/build/utils/PresetsManager.js +148 -0
  139. package/build/utils/SemanticsMapProtocol.js +144 -0
  140. package/build/utils/URLHandler.js +426 -0
  141. package/build/utils/geocoder.js +203 -0
  142. package/build/utils/i18n.js +128 -0
  143. package/build/utils/index.js +17 -0
  144. package/build/utils/index_photoviewer.js +14 -0
  145. package/build/utils/indoor.js +200 -0
  146. package/build/utils/map.js +788 -0
  147. package/build/utils/picture.js +507 -0
  148. package/build/utils/semantics.js +321 -0
  149. package/build/utils/services.js +148 -0
  150. package/build/utils/utils.js +433 -0
  151. package/build/utils/widgets.js +110 -0
  152. package/package.json +1 -1
@@ -0,0 +1,507 @@
1
+ import { COLORS, getArrow, getDistance, getSimplifiedAngle, svgToPSVLink } from "./utils.js";
2
+ const ArrowTriangleSVG = await fetch(new URL("../img/arrow_triangle.svg", import.meta.url).href).then(res => res.text());
3
+ const ArrowTurnSVG = await fetch(new URL("../img/arrow_turn.svg", import.meta.url).href).then(res => res.text());
4
+ const ArrowTriangle = svgToPSVLink(ArrowTriangleSVG, "white");
5
+ const ArrowTurn = svgToPSVLink(ArrowTurnSVG, COLORS.NEXT);
6
+
7
+ /**
8
+ * Read float value from EXIF tags (to handle fractions & all)
9
+ * @param {*} val The input EXIF tag value
10
+ * @returns {number|undefined} The parsed value, or undefined if value is not readable
11
+ * @private
12
+ */
13
+ export function getExifFloat(val) {
14
+ // Null-like values
15
+ if(
16
+ [null, undefined, ""].includes(val)
17
+ || typeof val === "string" && val.trim() === ""
18
+ ) {
19
+ return undefined;
20
+ }
21
+ // Already valid number
22
+ else if(typeof val === "number") {
23
+ return val;
24
+ }
25
+ // String
26
+ else if(typeof val === "string") {
27
+ // Check if looks like a fraction
28
+ if(/^-?\d+(\.\d+)?\/-?\d+(\.\d+)?$/.test(val)) {
29
+ const parts = val.split("/").map(p => parseFloat(p));
30
+ return parts[0] / parts[1];
31
+ }
32
+
33
+ // Try a direct cast to float
34
+ try { return parseFloat(val); }
35
+
36
+ catch(e) {}
37
+
38
+ // Unrecognized
39
+ return undefined;
40
+ }
41
+ else { return undefined; }
42
+ }
43
+
44
+ /**
45
+ * Find in picture metadata the GPS precision.
46
+ * @param {object} picture The GeoJSON picture feature
47
+ * @returns {string} The precision value (poor, fair, moderate, good, excellent, ideal, unknown)
48
+ * @private
49
+ */
50
+ export function getGPSPrecision(picture) {
51
+ let quality = null;
52
+ const gpsHPosError = picture?.properties?.["quality:horizontal_accuracy"] || getExifFloat(picture?.properties?.exif?.["Exif.GPSInfo.GPSHPositioningError"]);
53
+ const gpsDop = getExifFloat(picture?.properties?.exif?.["Exif.GPSInfo.GPSDOP"]);
54
+
55
+ if(gpsHPosError !== undefined) {
56
+ quality = `${gpsHPosError} m`;
57
+ }
58
+ else if(gpsDop !== undefined) {
59
+ if(gpsDop < 1) { quality = "ideal"; }
60
+ else if(gpsDop < 2) { quality = "excellent"; }
61
+ else if(gpsDop < 5) { quality = "good"; }
62
+ else if(gpsDop < 10) { quality = "moderate"; }
63
+ else if(gpsDop < 20) { quality = "fair"; }
64
+ else { quality = "poor"; }
65
+ }
66
+
67
+ return quality;
68
+ }
69
+
70
+ /**
71
+ * Compute PSV sphere correction based on picture metadata & EXIF tags.
72
+ * @param {object} picture The GeoJSON picture feature
73
+ * @returns {object} The PSV sphereCorrection value
74
+ * @private
75
+ */
76
+ export function getSphereCorrection(picture) {
77
+ // Photo direction
78
+ let dir = picture.properties?.["view:azimuth"];
79
+ if(dir === undefined) {
80
+ const v = getExifFloat(picture.properties?.exif?.["Exif.GPSInfo.GPSImgDirection"]);
81
+ if(v !== undefined) {
82
+ dir = v;
83
+ }
84
+ }
85
+ dir = dir || 0;
86
+
87
+ // Yaw
88
+ let yaw = picture.properties?.["pers:yaw"];
89
+ let exifFallbacks = ["Xmp.GPano.PoseHeadingDegrees", "Xmp.Camera.Yaw", "Exif.MpfInfo.MPFYawAngle"];
90
+ if(yaw === undefined) {
91
+ for(let exif of exifFallbacks) {
92
+ const v = getExifFloat(picture.properties?.exif?.[exif]);
93
+ if(v !== undefined) {
94
+ yaw = v;
95
+ break;
96
+ }
97
+ }
98
+ }
99
+ yaw = yaw || 0;
100
+
101
+ // Check if yaw is applicable: different from photo direction
102
+ if(Math.round(dir) === Math.round(yaw) && yaw > 0) {
103
+ console.warn("Picture with UUID", picture.id, "has same GPS Image direction and Yaw, could cause rendering issues");
104
+ // Yaw = 0;
105
+ }
106
+
107
+ // Pitch
108
+ let pitch = picture.properties?.["pers:pitch"];
109
+ exifFallbacks = ["Xmp.GPano.PosePitchDegrees", "Xmp.Camera.Pitch", "Exif.MpfInfo.MPFPitchAngle"];
110
+ if(pitch === undefined) {
111
+ for(let exif of exifFallbacks) {
112
+ const v = getExifFloat(picture.properties?.exif?.[exif]);
113
+ if(v !== undefined) {
114
+ pitch = v;
115
+ break;
116
+ }
117
+ }
118
+ }
119
+ pitch = pitch || 0;
120
+
121
+ // Roll
122
+ let roll = picture.properties?.["pers:roll"];
123
+ exifFallbacks = ["Xmp.GPano.PoseRollDegrees", "Xmp.Camera.Roll", "Exif.MpfInfo.MPFRollAngle"];
124
+ if(roll === undefined) {
125
+ for(let exif of exifFallbacks) {
126
+ const v = getExifFloat(picture.properties?.exif?.[exif]);
127
+ if(v !== undefined) {
128
+ roll = v;
129
+ break;
130
+ }
131
+ }
132
+ }
133
+ roll = roll || 0;
134
+
135
+ // Send result
136
+ return pitch !== 0 && roll !== 0 ? {
137
+ pan: yaw * Math.PI / 180,
138
+ tilt: -pitch * Math.PI / 180,
139
+ roll: roll * Math.PI / 180,
140
+ } : {};
141
+ }
142
+
143
+ /**
144
+ * Compute PSV panoData for cropped panorama based on picture metadata & EXIF tags.
145
+ * @param {object} picture The GeoJSON picture feature
146
+ * @param {object} [img] The loaded image file, with width, height properties for possible resizing
147
+ * @returns {object} The PSV panoData values
148
+ * @private
149
+ */
150
+ export function getCroppedPanoData(picture, img) {
151
+ let res;
152
+
153
+ if(picture.properties?.["pers:interior_orientation"]) {
154
+ if(
155
+ picture.properties["pers:interior_orientation"]?.["visible_area"]
156
+ && picture.properties["pers:interior_orientation"]?.["sensor_array_dimensions"]
157
+ ) {
158
+ const va = picture.properties["pers:interior_orientation"]["visible_area"];
159
+ const sad = picture.properties["pers:interior_orientation"]["sensor_array_dimensions"];
160
+ try {
161
+ res = {
162
+ fullWidth: parseInt(sad[0]),
163
+ fullHeight: parseInt(sad[1]),
164
+ croppedX: parseInt(va[0]),
165
+ croppedY: parseInt(va[1]),
166
+ croppedWidth: parseInt(sad[0]) - parseInt(va[2]) - parseInt(va[0]),
167
+ croppedHeight: parseInt(sad[1]) - parseInt(va[3]) - parseInt(va[1]),
168
+ };
169
+ }
170
+
171
+ catch(e) {
172
+ console.warn("Invalid pers:interior_orientation values for cropped panorama "+picture.id);
173
+ }
174
+ }
175
+ }
176
+
177
+ if(!res && picture.properties?.exif) {
178
+ try {
179
+ res = {
180
+ fullWidth: parseInt(picture.properties.exif?.["Xmp.GPano.FullPanoWidthPixels"]),
181
+ fullHeight: parseInt(picture.properties.exif?.["Xmp.GPano.FullPanoHeightPixels"]),
182
+ croppedX: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaLeftPixels"]),
183
+ croppedY: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaTopPixels"]),
184
+ croppedWidth: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaImageWidthPixels"]),
185
+ croppedHeight: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaImageHeightPixels"]),
186
+ };
187
+
188
+ // Fix images with cropped size higher than full size
189
+ if(res.croppedWidth > res.fullWidth) { res.fullWidth = res.croppedWidth; }
190
+ if(res.croppedHeight > res.fullHeight) { res.fullHeight = res.croppedHeight; }
191
+ }
192
+
193
+ catch(e) {
194
+ console.warn("Invalid XMP.GPano values for cropped panorama "+picture.id);
195
+ }
196
+ }
197
+
198
+ // Check if crop is really necessary
199
+ if(res) {
200
+ res = Object.fromEntries(Object.entries(res || {}).filter(e => !isNaN(e[1])));
201
+ if(
202
+ (!res.fullWidth && !res.croppedWidth && res.fullHeight && !res.croppedHeight)
203
+ || (res.fullWidth && !res.croppedWidth && !res.fullHeight && !res.croppedHeight)
204
+ || (res.fullWidth && !res.croppedWidth && res.fullHeight && !res.croppedHeight)
205
+ // eslint-disable-next-line eqeqeq
206
+ || (res.fullWidth == res.croppedWidth && res.fullHeight == res.croppedHeight)
207
+ ) {
208
+ res = {};
209
+ }
210
+ }
211
+
212
+ // Resize if image given
213
+ if(res?.fullWidth && img?.width) {
214
+ let ratio = img.width / (res?.croppedWidth || res.fullWidth);
215
+ res = {
216
+ fullWidth: res.fullWidth !== undefined ? Math.floor(res.fullWidth * ratio) : undefined,
217
+ fullHeight: res.fullHeight !== undefined ? Math.floor(res.fullHeight * ratio) : undefined,
218
+ croppedWidth: res.croppedWidth !== undefined ? Math.floor(res.croppedWidth * ratio) : undefined,
219
+ croppedHeight: res.croppedHeight !== undefined ? Math.floor(res.croppedHeight * ratio) : undefined,
220
+ croppedX: res.croppedX !== undefined ? Math.floor(res.croppedX * ratio) : undefined,
221
+ croppedY: res.croppedY !== undefined ? Math.floor(res.croppedY * ratio) : undefined,
222
+ };
223
+ }
224
+
225
+ return res || {};
226
+ }
227
+
228
+ /**
229
+ * Compare function to retrieve most appropriate picture in a single direction.
230
+ *
231
+ * @param {number[]} picPos The picture [x,y] position
232
+ * @returns {function} A compare function for sorting
233
+ * @private
234
+ */
235
+ export function sortPicturesInDirection(picPos) {
236
+ return (a,b) => {
237
+ // Two prev/next links = no sort
238
+ if(a.rel !== "related" && b.rel !== "related") { return 0; }
239
+ // First is prev/next link = goes first
240
+ else if(a.rel !== "related") { return -1; }
241
+ // Second is prev/next link = goes first
242
+ else if(b.rel !== "related") { return 1; }
243
+ // Two related links same day = nearest goes first
244
+ else if(a.date === b.date) { return getDistance(picPos, a.geometry.coordinates) - getDistance(picPos, b.geometry.coordinates); }
245
+ // Two related links at different day = recent goes first
246
+ else { return b.date.localeCompare(a.date); }
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Generates the navbar caption based on a single picture metadata
252
+ *
253
+ * @param {object} metadata The picture metadata
254
+ * @param {object} t The labels translations container
255
+ * @returns {object} Normalized object with user name, licence and date
256
+ * @private
257
+ */
258
+ export function getNodeCaption(metadata, t) {
259
+ const caption = {};
260
+
261
+ // Timestamp
262
+ if(metadata?.properties?.datetimetz) {
263
+ caption.date = new Date(metadata.properties.datetimetz);
264
+ const timeZoneMatch = metadata.properties.datetimetz.match(/([+-]\d{2}):(\d{2})$|Z$/);
265
+ if (timeZoneMatch) {
266
+ if (timeZoneMatch[0] === "Z") {
267
+ caption.tz = "UTC";
268
+ } else {
269
+ caption.tz = timeZoneMatch[0];
270
+ }
271
+ }
272
+ }
273
+ else if(metadata?.properties?.datetime) {
274
+ caption.date = new Date(metadata.properties.datetime);
275
+ }
276
+
277
+ // Producer
278
+ if(metadata?.providers) {
279
+ const producerRoles = metadata?.providers?.filter(el => el?.roles?.includes("producer"));
280
+ if(producerRoles?.length >= 0) {
281
+ // Avoid duplicates between account name and picture author
282
+ const producersDeduped = {};
283
+ producerRoles.map(p => p.name).forEach(p => {
284
+ const pmin = p.toLowerCase().replace(/\s/g, "");
285
+ if(producersDeduped[pmin]) { producersDeduped[pmin].push(p); }
286
+ else { producersDeduped[pmin] = [p];}
287
+ });
288
+
289
+ // Keep best looking name for each
290
+ caption.producer = [];
291
+ Object.values(producersDeduped).forEach(pv => {
292
+ const deflt = pv[0];
293
+ const better = pv.find(v => v.toLowerCase() !== v);
294
+ caption.producer.push(better || deflt);
295
+ });
296
+ }
297
+ }
298
+
299
+ // License
300
+ if(metadata?.properties?.license) {
301
+ caption.license = metadata.properties.license;
302
+ // Look for URL to license
303
+ if(metadata?.links) {
304
+ const licenseLink = metadata.links.find(l => l?.rel === "license");
305
+ if(licenseLink) {
306
+ caption.license = `<a href="${licenseLink.href}" title="${t.pnx.metadata_general_license_link}" target="_blank">${caption.license}</a>`;
307
+ }
308
+ }
309
+ }
310
+
311
+ return caption;
312
+ }
313
+
314
+ /**
315
+ * Transforms a GeoJSON feature from the STAC API into a PSV node.
316
+ *
317
+ * @param {object} f The API GeoJSON feature
318
+ * @param {object} t The labels translations container
319
+ * @param {boolean} [fastInternet] True if Internet speed is high enough for loading HD flat pictures
320
+ * @param {function} [customLinkFilter] A function checking if a STAC link is acceptable to use for picture navigation
321
+ * @param {function} [urlCleaner] A function that each URL goes through for cleaning (allows use of relative URLs)
322
+ * @return {object} A PSV node
323
+ * @private
324
+ */
325
+ export function apiFeatureToPSVNode(f, t, fastInternet=false, customLinkFilter=null, urlCleaner=(u => u)) {
326
+ // eslint-disable-next-line eqeqeq
327
+ const isHorizontalFovDefined = f.properties?.["pers:interior_orientation"]?.["field_of_view"] != null;
328
+ let horizontalFov = isHorizontalFovDefined ? parseInt(f.properties["pers:interior_orientation"]["field_of_view"]) : 70;
329
+ const is360 = horizontalFov === 360;
330
+
331
+ const hdUrl = urlCleaner((Object.values(f.assets).find(a => a?.roles?.includes("data")) || {}).href);
332
+ const matrix = f?.properties?.["tiles:tile_matrix_sets"]?.geovisio;
333
+ const prev = f.links?.find?.(l => l?.rel === "prev" && l?.type === "application/geo+json");
334
+ const next = f.links?.find?.(l => l?.rel === "next" && l?.type === "application/geo+json");
335
+ const baseUrlWebp = Object.values(f.assets).find(a => a.roles?.includes("visual") && a.type === "image/webp");
336
+ const baseUrlJpeg = Object.values(f.assets).find(a => a.roles?.includes("visual") && a.type === "image/jpeg");
337
+ const baseUrl = urlCleaner((baseUrlWebp || baseUrlJpeg).href);
338
+ const thumbUrl = urlCleaner((Object.values(f.assets).find(a => a.roles?.includes("thumbnail") && a.type === "image/jpeg"))?.href);
339
+ const tileUrl = f?.asset_templates?.tiles_webp || f?.asset_templates?.tiles;
340
+ const croppedPanoData = getCroppedPanoData(f);
341
+ const origInstance = f.links?.find?.(l => l?.rel === "via" && l?.type === "application/json");
342
+ const size = f.properties?.["pers:interior_orientation"]?.["sensor_array_dimensions"];
343
+
344
+ let panorama;
345
+
346
+ // Cropped panorama
347
+ if(Object.keys(croppedPanoData).length > 0) {
348
+ panorama = {
349
+ baseUrl: fastInternet ? hdUrl : baseUrl,
350
+ origBaseUrl: fastInternet ? hdUrl : baseUrl,
351
+ hdUrl,
352
+ thumbUrl,
353
+ basePanoData: fastInternet ? croppedPanoData : (img) => getCroppedPanoData(f, img),
354
+ cropData: croppedPanoData,
355
+ width: croppedPanoData.fullWidth,
356
+ // This is only to mock loading of tiles (which are not available for flat pictures)
357
+ cols: 2, rows: 1, tileUrl: () => null
358
+ };
359
+ }
360
+ // 360°
361
+ else if(is360 && matrix) {
362
+ panorama = {
363
+ baseUrl,
364
+ origBaseUrl: baseUrl,
365
+ basePanoData: (img) => ({
366
+ fullWidth: img.width,
367
+ fullHeight: img.height,
368
+ }),
369
+ hdUrl,
370
+ thumbUrl,
371
+ cols: matrix && matrix.tileMatrix[0].matrixWidth,
372
+ rows: matrix && matrix.tileMatrix[0].matrixHeight,
373
+ width: matrix && (matrix.tileMatrix[0].matrixWidth * matrix.tileMatrix[0].tileWidth),
374
+ tileUrl: matrix && ((col, row) => urlCleaner(tileUrl.href.replace(/\{TileCol\}/g, col).replace(/\{TileRow\}/g, row)))
375
+ };
376
+ }
377
+ // Flat pictures: shown only using a cropped base panorama
378
+ else {
379
+ const getFlatCrop = (img) => {
380
+ if (img.width < img.height && !isHorizontalFovDefined) {
381
+ horizontalFov = 35;
382
+ }
383
+ const verticalFov = horizontalFov * img.height / img.width;
384
+ const panoWidth = img.width * 360 / horizontalFov;
385
+ const panoHeight = img.height * 180 / verticalFov;
386
+
387
+ return {
388
+ fullWidth: Math.floor(panoWidth),
389
+ fullHeight: Math.floor(panoHeight),
390
+ croppedWidth: img.width,
391
+ croppedHeight: img.height,
392
+ croppedX: Math.floor((panoWidth - img.width) / 2),
393
+ croppedY: Math.floor((panoHeight - img.height) / 2),
394
+ };
395
+ };
396
+
397
+ let cropData = size ? getFlatCrop({width: size[0], height: size[1]}) : null;
398
+
399
+ panorama = {
400
+ baseUrl: fastInternet ? hdUrl : baseUrl,
401
+ origBaseUrl: fastInternet ? hdUrl : baseUrl,
402
+ hdUrl,
403
+ thumbUrl,
404
+ basePanoData: getFlatCrop,
405
+ cropData,
406
+ width: cropData?.fullWidth || 2,
407
+ // This is only to mock loading of tiles (which are not available for flat pictures)
408
+ cols: 2, rows: 1, tileUrl: () => null
409
+ };
410
+ }
411
+
412
+ // Cleanup empty semantics feature (metacatalog bug)
413
+ if(f.properties?.semantics) {
414
+ f.properties.semantics = f.properties.semantics.filter(o => Object.keys(o).length > 0);
415
+ }
416
+
417
+ const node = {
418
+ id: f.id,
419
+ caption: getNodeCaption(f, t),
420
+ panorama,
421
+ links: filterRelatedPicsLinks(f, customLinkFilter),
422
+ gps: f.geometry.coordinates,
423
+ sequence: {
424
+ id: f.collection,
425
+ nextPic: next ? next.id : undefined,
426
+ prevPic: prev ? prev.id : undefined
427
+ },
428
+ sphereCorrection: getSphereCorrection(f),
429
+ horizontalFov,
430
+ origInstance,
431
+ properties: f.properties,
432
+ };
433
+
434
+ return node;
435
+ }
436
+
437
+ /**
438
+ * Filter surrounding pictures links to avoid too much arrows on viewer.
439
+ * @private
440
+ */
441
+ export function filterRelatedPicsLinks(metadata, customFilter = null) {
442
+ const picLinks = metadata.links
443
+ .filter(l => ["next", "prev", "related"].includes(l?.rel) && l?.type === "application/geo+json")
444
+ .filter(l => customFilter ? customFilter(l) : true)
445
+ .map(l => {
446
+ if(l.datetime) {
447
+ l.date = l.datetime.split("T")[0];
448
+ }
449
+ return l;
450
+ });
451
+ const picPos = metadata.geometry.coordinates;
452
+
453
+ // Filter to keep a single link per direction, in same sequence or most recent one
454
+ const filteredLinks = [];
455
+ const picSurroundings = { "N": [], "ENE": [], "ESE": [], "S": [], "WSW": [], "WNW": [] };
456
+
457
+ for(let picLink of picLinks) {
458
+ const a = getSimplifiedAngle(picPos, picLink.geometry.coordinates);
459
+ picSurroundings[a].push(picLink);
460
+ }
461
+
462
+ for(let direction in picSurroundings) {
463
+ const picsInDirection = picSurroundings[direction];
464
+ if(picsInDirection.length === 0) { continue; }
465
+ picsInDirection.sort(sortPicturesInDirection(picPos));
466
+ filteredLinks.push(picsInDirection.shift());
467
+ }
468
+
469
+ let arrowStyle = l => l.rel === "related" ? {
470
+ element: getArrow(ArrowTurn),
471
+ size: { width: 64*2/3, height: 192*2/3 }
472
+ } : {
473
+ element: getArrow(ArrowTriangle),
474
+ size: { width: 75, height: 75 }
475
+ };
476
+
477
+ const rectifiedYaw = - (metadata.properties?.["view:azimuth"] || 0) * (Math.PI / 180);
478
+ return filteredLinks.map(l => ({
479
+ nodeId: l.id,
480
+ gps: l.geometry.coordinates,
481
+ arrowStyle: arrowStyle(l),
482
+ linkOffset: { yaw: rectifiedYaw }
483
+ }));
484
+ }
485
+
486
+ /**
487
+ * Read structured hashtags from picture metadata
488
+ * @private
489
+ */
490
+ export function getHashTags(metadata) {
491
+ if(!metadata?.properties?.semantics) { return []; }
492
+
493
+ // Find hashtag entry in semantics
494
+ const hashtagsTags = metadata.properties.semantics.filter(kv => kv.key === "hashtags");
495
+ const readHashTags = [];
496
+ hashtagsTags.forEach(htt => htt.value.split(";").forEach(v => readHashTags.push(v.trim())));
497
+
498
+ return readHashTags;
499
+ }
500
+
501
+ /**
502
+ * Check if a given picture has associated annotations.
503
+ * @private
504
+ */
505
+ export function hasAnnotations(metadata) {
506
+ return metadata?.properties?.annotations?.length > 0;
507
+ }