@panoramax/web-viewer 3.2.3 → 4.0.0

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 (255) hide show
  1. package/.gitlab-ci.yml +13 -6
  2. package/CHANGELOG.md +49 -1
  3. package/CODE_OF_CONDUCT.md +1 -1
  4. package/README.md +1 -1
  5. package/build/editor.html +10 -1
  6. package/build/index.css +12 -12
  7. package/build/index.css.map +1 -1
  8. package/build/index.html +1 -1
  9. package/build/index.js +2126 -14
  10. package/build/index.js.map +1 -1
  11. package/build/map.html +1 -1
  12. package/build/photo.html +1 -0
  13. package/build/static/media/atkinson-hyperlegible-next-latin-400-normal..woff +0 -0
  14. package/build/static/media/atkinson-hyperlegible-next-latin-400-normal..woff2 +0 -0
  15. package/build/static/media/atkinson-hyperlegible-next-latin-ext-400-normal..woff +0 -0
  16. package/build/static/media/atkinson-hyperlegible-next-latin-ext-400-normal..woff2 +0 -0
  17. package/build/viewer.html +12 -1
  18. package/build/widgets.html +1 -0
  19. package/config/jest/mocks.js +201 -0
  20. package/config/paths.js +2 -0
  21. package/config/webpack.config.js +52 -0
  22. package/docs/03_URL_settings.md +14 -16
  23. package/docs/05_Compatibility.md +59 -76
  24. package/docs/09_Develop.md +46 -11
  25. package/docs/90_Releases.md +2 -2
  26. package/docs/images/class_diagram.drawio +60 -45
  27. package/docs/images/class_diagram.jpg +0 -0
  28. package/docs/images/screenshot.jpg +0 -0
  29. package/docs/index.md +135 -0
  30. package/docs/reference/components/core/Basic.md +196 -0
  31. package/docs/reference/components/core/CoverageMap.md +210 -0
  32. package/docs/reference/components/core/Editor.md +224 -0
  33. package/docs/reference/components/core/PhotoViewer.md +307 -0
  34. package/docs/reference/components/core/Viewer.md +350 -0
  35. package/docs/reference/components/layout/BottomDrawer.md +35 -0
  36. package/docs/reference/components/layout/CorneredGrid.md +29 -0
  37. package/docs/reference/components/layout/Mini.md +45 -0
  38. package/docs/reference/components/layout/Tabs.md +45 -0
  39. package/docs/reference/components/menus/MapBackground.md +32 -0
  40. package/docs/reference/components/menus/MapFilters.md +15 -0
  41. package/docs/reference/components/menus/MapLayers.md +15 -0
  42. package/docs/reference/components/menus/MapLegend.md +15 -0
  43. package/docs/reference/components/menus/PictureLegend.md +16 -0
  44. package/docs/reference/components/menus/PictureMetadata.md +15 -0
  45. package/docs/reference/components/menus/PlayerOptions.md +15 -0
  46. package/docs/reference/components/menus/QualityScoreDoc.md +15 -0
  47. package/docs/reference/components/menus/ReportForm.md +15 -0
  48. package/docs/reference/components/menus/ShareMenu.md +15 -0
  49. package/docs/reference/components/ui/Button.md +40 -0
  50. package/docs/reference/components/ui/ButtonGroup.md +36 -0
  51. package/docs/reference/components/ui/CopyButton.md +38 -0
  52. package/docs/reference/components/ui/Grade.md +32 -0
  53. package/docs/reference/components/ui/LinkButton.md +45 -0
  54. package/docs/reference/components/ui/ListGroup.md +22 -0
  55. package/docs/reference/components/ui/Loader.md +56 -0
  56. package/docs/reference/components/ui/Map.md +239 -0
  57. package/docs/reference/components/ui/MapMore.md +256 -0
  58. package/docs/reference/components/ui/Photo.md +385 -0
  59. package/docs/reference/components/ui/Popup.md +56 -0
  60. package/docs/reference/components/ui/ProgressBar.md +32 -0
  61. package/docs/reference/components/ui/QualityScore.md +45 -0
  62. package/docs/reference/components/ui/SearchBar.md +63 -0
  63. package/docs/reference/components/ui/TogglableGroup.md +39 -0
  64. package/docs/reference/components/ui/widgets/GeoSearch.md +32 -0
  65. package/docs/reference/components/ui/widgets/Legend.md +49 -0
  66. package/docs/reference/components/ui/widgets/MapFiltersButton.md +33 -0
  67. package/docs/reference/components/ui/widgets/MapLayersButton.md +15 -0
  68. package/docs/reference/components/ui/widgets/OSMEditors.md +15 -0
  69. package/docs/reference/components/ui/widgets/PictureLegendActions.md +32 -0
  70. package/docs/reference/components/ui/widgets/Player.md +33 -0
  71. package/docs/reference/components/ui/widgets/Zoom.md +15 -0
  72. package/docs/reference/utils/API.md +334 -0
  73. package/docs/reference/utils/InitParameters.md +68 -0
  74. package/docs/reference/utils/URLHandler.md +107 -0
  75. package/docs/reference.md +79 -0
  76. package/docs/shortcuts.md +11 -0
  77. package/docs/tutorials/aerial_imagery.md +19 -0
  78. package/docs/tutorials/authentication.md +10 -0
  79. package/docs/tutorials/custom_widgets.md +59 -0
  80. package/docs/tutorials/map_style.md +39 -0
  81. package/docs/tutorials/migrate_v4.md +153 -0
  82. package/docs/tutorials/synced_coverage.md +43 -0
  83. package/mkdocs.yml +66 -5
  84. package/package.json +22 -17
  85. package/public/editor.html +21 -29
  86. package/public/index.html +17 -12
  87. package/public/map.html +19 -18
  88. package/public/photo.html +55 -0
  89. package/public/viewer.html +22 -26
  90. package/public/widgets.html +306 -0
  91. package/scripts/doc.js +79 -0
  92. package/src/components/core/Basic.css +48 -0
  93. package/src/components/core/Basic.js +349 -0
  94. package/src/components/core/CoverageMap.css +9 -0
  95. package/src/components/core/CoverageMap.js +139 -0
  96. package/src/components/core/Editor.css +23 -0
  97. package/src/components/core/Editor.js +390 -0
  98. package/src/components/core/PhotoViewer.css +48 -0
  99. package/src/components/core/PhotoViewer.js +499 -0
  100. package/src/components/core/Viewer.css +98 -0
  101. package/src/components/core/Viewer.js +564 -0
  102. package/src/components/core/index.js +12 -0
  103. package/src/components/index.js +13 -0
  104. package/src/components/layout/BottomDrawer.js +257 -0
  105. package/src/components/layout/CorneredGrid.js +112 -0
  106. package/src/components/layout/Mini.js +117 -0
  107. package/src/components/layout/Tabs.js +133 -0
  108. package/src/components/layout/index.js +9 -0
  109. package/src/components/menus/MapBackground.js +106 -0
  110. package/src/components/menus/MapFilters.js +400 -0
  111. package/src/components/menus/MapLayers.js +143 -0
  112. package/src/components/menus/MapLegend.js +34 -0
  113. package/src/components/menus/PictureLegend.js +253 -0
  114. package/src/components/menus/PictureMetadata.js +317 -0
  115. package/src/components/menus/PlayerOptions.js +95 -0
  116. package/src/components/menus/QualityScoreDoc.js +36 -0
  117. package/src/components/menus/ReportForm.js +133 -0
  118. package/src/components/menus/Share.js +100 -0
  119. package/src/components/menus/index.js +15 -0
  120. package/src/components/styles.js +383 -0
  121. package/src/components/ui/Button.js +77 -0
  122. package/src/components/ui/ButtonGroup.css +57 -0
  123. package/src/components/ui/ButtonGroup.js +68 -0
  124. package/src/components/ui/CopyButton.js +106 -0
  125. package/src/components/ui/Grade.js +54 -0
  126. package/src/components/ui/LinkButton.js +67 -0
  127. package/src/components/ui/ListGroup.js +66 -0
  128. package/src/components/ui/Loader.js +203 -0
  129. package/src/components/{Map.css → ui/Map.css} +5 -17
  130. package/src/components/{Map.js → ui/Map.js} +148 -156
  131. package/src/components/ui/MapMore.js +324 -0
  132. package/src/components/{Photo.css → ui/Photo.css} +6 -6
  133. package/src/components/{Photo.js → ui/Photo.js} +313 -101
  134. package/src/components/ui/Popup.js +145 -0
  135. package/src/components/ui/ProgressBar.js +104 -0
  136. package/src/components/ui/QualityScore.js +147 -0
  137. package/src/components/ui/SearchBar.js +367 -0
  138. package/src/components/ui/TogglableGroup.js +157 -0
  139. package/src/components/ui/index.js +22 -0
  140. package/src/components/ui/widgets/GeoSearch.css +21 -0
  141. package/src/components/ui/widgets/GeoSearch.js +139 -0
  142. package/src/components/ui/widgets/Legend.js +113 -0
  143. package/src/components/ui/widgets/MapFiltersButton.js +104 -0
  144. package/src/components/ui/widgets/MapLayersButton.js +79 -0
  145. package/src/components/ui/widgets/OSMEditors.js +155 -0
  146. package/src/components/ui/widgets/PictureLegendActions.js +117 -0
  147. package/src/components/ui/widgets/Player.css +7 -0
  148. package/src/components/ui/widgets/Player.js +151 -0
  149. package/src/components/ui/widgets/Zoom.js +82 -0
  150. package/src/components/ui/widgets/index.js +13 -0
  151. package/src/img/loader_base.jpg +0 -0
  152. package/src/img/panoramax.svg +13 -0
  153. package/src/img/switch_big.svg +20 -10
  154. package/src/index.js +7 -9
  155. package/src/translations/br.json +1 -0
  156. package/src/translations/da.json +38 -15
  157. package/src/translations/de.json +5 -3
  158. package/src/translations/en.json +35 -15
  159. package/src/translations/eo.json +38 -15
  160. package/src/translations/es.json +1 -1
  161. package/src/translations/fr.json +36 -16
  162. package/src/translations/hu.json +1 -1
  163. package/src/translations/it.json +39 -16
  164. package/src/translations/ja.json +182 -1
  165. package/src/translations/nl.json +106 -6
  166. package/src/translations/pl.json +1 -1
  167. package/src/translations/sv.json +182 -0
  168. package/src/translations/zh_Hant.json +35 -14
  169. package/src/utils/API.js +109 -49
  170. package/src/utils/InitParameters.js +388 -0
  171. package/src/utils/PhotoAdapter.js +1 -0
  172. package/src/utils/URLHandler.js +362 -0
  173. package/src/utils/geocoder.js +152 -0
  174. package/src/utils/{I18n.js → i18n.js} +7 -3
  175. package/src/utils/index.js +11 -0
  176. package/src/utils/{Map.js → map.js} +256 -77
  177. package/src/utils/picture.js +442 -0
  178. package/src/utils/utils.js +324 -0
  179. package/src/utils/widgets.js +55 -0
  180. package/tests/components/core/Basic.test.js +121 -0
  181. package/tests/components/core/BasicMock.js +25 -0
  182. package/tests/components/core/CoverageMap.test.js +20 -0
  183. package/tests/components/core/Editor.test.js +20 -0
  184. package/tests/components/core/PhotoViewer.test.js +57 -0
  185. package/tests/components/core/Viewer.test.js +84 -0
  186. package/tests/components/core/__snapshots__/PhotoViewer.test.js.snap +73 -0
  187. package/tests/components/core/__snapshots__/Viewer.test.js.snap +145 -0
  188. package/tests/components/ui/CopyButton.test.js +52 -0
  189. package/tests/components/ui/Loader.test.js +55 -0
  190. package/tests/components/{Map.test.js → ui/Map.test.js} +73 -61
  191. package/tests/components/{Photo.test.js → ui/Photo.test.js} +97 -63
  192. package/tests/components/ui/Popup.test.js +26 -0
  193. package/tests/components/ui/QualityScore.test.js +18 -0
  194. package/tests/components/ui/SearchBar.test.js +110 -0
  195. package/tests/components/ui/__snapshots__/CopyButton.test.js.snap +33 -0
  196. package/tests/components/ui/__snapshots__/Loader.test.js.snap +56 -0
  197. package/tests/components/{__snapshots__ → ui/__snapshots__}/Map.test.js.snap +11 -38
  198. package/tests/components/{__snapshots__ → ui/__snapshots__}/Photo.test.js.snap +70 -6
  199. package/tests/components/ui/__snapshots__/Popup.test.js.snap +29 -0
  200. package/tests/components/ui/__snapshots__/QualityScore.test.js.snap +11 -0
  201. package/tests/components/ui/__snapshots__/SearchBar.test.js.snap +65 -0
  202. package/tests/utils/API.test.js +83 -83
  203. package/tests/utils/InitParameters.test.js +499 -0
  204. package/tests/utils/URLHandler.test.js +401 -0
  205. package/tests/utils/__snapshots__/API.test.js.snap +10 -0
  206. package/tests/utils/__snapshots__/URLHandler.test.js.snap +21 -0
  207. package/tests/utils/__snapshots__/{Map.test.js.snap → geocoder.test.js.snap} +1 -1
  208. package/tests/utils/__snapshots__/map.test.js.snap +11 -0
  209. package/tests/utils/__snapshots__/picture.test.js.snap +327 -0
  210. package/tests/utils/__snapshots__/widgets.test.js.snap +19 -0
  211. package/tests/utils/geocoder.test.js +37 -0
  212. package/tests/utils/{I18n.test.js → i18n.test.js} +8 -8
  213. package/tests/utils/map.test.js +126 -0
  214. package/tests/utils/picture.test.js +745 -0
  215. package/tests/utils/utils.test.js +288 -0
  216. package/tests/utils/widgets.test.js +31 -0
  217. package/docs/01_Start.md +0 -149
  218. package/docs/02_Usage.md +0 -831
  219. package/docs/04_Advanced_examples.md +0 -216
  220. package/src/Editor.css +0 -37
  221. package/src/Editor.js +0 -361
  222. package/src/StandaloneMap.js +0 -114
  223. package/src/Viewer.css +0 -203
  224. package/src/Viewer.js +0 -1246
  225. package/src/components/CoreView.css +0 -70
  226. package/src/components/CoreView.js +0 -175
  227. package/src/components/Loader.css +0 -74
  228. package/src/components/Loader.js +0 -120
  229. package/src/img/loader_hd.jpg +0 -0
  230. package/src/utils/Exif.js +0 -193
  231. package/src/utils/Utils.js +0 -631
  232. package/src/utils/Widgets.js +0 -562
  233. package/src/viewer/URLHash.js +0 -469
  234. package/src/viewer/Widgets.css +0 -880
  235. package/src/viewer/Widgets.js +0 -1470
  236. package/tests/Editor.test.js +0 -126
  237. package/tests/StandaloneMap.test.js +0 -45
  238. package/tests/Viewer.test.js +0 -366
  239. package/tests/__snapshots__/Editor.test.js.snap +0 -298
  240. package/tests/__snapshots__/StandaloneMap.test.js.snap +0 -30
  241. package/tests/__snapshots__/Viewer.test.js.snap +0 -195
  242. package/tests/components/CoreView.test.js +0 -92
  243. package/tests/components/Loader.test.js +0 -38
  244. package/tests/components/__snapshots__/Loader.test.js.snap +0 -15
  245. package/tests/utils/Exif.test.js +0 -124
  246. package/tests/utils/Map.test.js +0 -113
  247. package/tests/utils/Utils.test.js +0 -300
  248. package/tests/utils/Widgets.test.js +0 -107
  249. package/tests/utils/__snapshots__/Exif.test.js.snap +0 -43
  250. package/tests/utils/__snapshots__/Utils.test.js.snap +0 -41
  251. package/tests/utils/__snapshots__/Widgets.test.js.snap +0 -44
  252. package/tests/viewer/URLHash.test.js +0 -559
  253. package/tests/viewer/Widgets.test.js +0 -127
  254. package/tests/viewer/__snapshots__/URLHash.test.js.snap +0 -108
  255. package/tests/viewer/__snapshots__/Widgets.test.js.snap +0 -403
@@ -0,0 +1,442 @@
1
+ import ArrowTriangleSVG from "../img/arrow_triangle.svg";
2
+ import ArrowTurnSVG from "../img/arrow_turn.svg";
3
+ import { svgToPSVLink, COLORS, getDistance, getSimplifiedAngle, getArrow } from "./utils";
4
+
5
+ const ArrowTriangle = svgToPSVLink(ArrowTriangleSVG, "white");
6
+ const ArrowTurn = svgToPSVLink(ArrowTurnSVG, COLORS.NEXT);
7
+
8
+ /**
9
+ * Read float value from EXIF tags (to handle fractions & all)
10
+ * @param {*} val The input EXIF tag value
11
+ * @returns {number|undefined} The parsed value, or undefined if value is not readable
12
+ * @private
13
+ */
14
+ export function getExifFloat(val) {
15
+ // Null-like values
16
+ if(
17
+ [null, undefined, ""].includes(val)
18
+ || typeof val === "string" && val.trim() === ""
19
+ ) {
20
+ return undefined;
21
+ }
22
+ // Already valid number
23
+ else if(typeof val === "number") {
24
+ return val;
25
+ }
26
+ // String
27
+ else if(typeof val === "string") {
28
+ // Check if looks like a fraction
29
+ if(/^-?\d+(\.\d+)?\/-?\d+(\.\d+)?$/.test(val)) {
30
+ const parts = val.split("/").map(p => parseFloat(p));
31
+ return parts[0] / parts[1];
32
+ }
33
+
34
+ // Try a direct cast to float
35
+ try { return parseFloat(val); }
36
+ catch(e) {} // eslint-disable-line no-empty
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
+ * @returns {object} The PSV panoData values
147
+ * @private
148
+ */
149
+ export function getCroppedPanoData(picture) {
150
+ let res;
151
+
152
+ if(picture.properties?.["pers:interior_orientation"]) {
153
+ if(
154
+ picture.properties["pers:interior_orientation"]?.["visible_area"]
155
+ && picture.properties["pers:interior_orientation"]?.["sensor_array_dimensions"]
156
+ ) {
157
+ const va = picture.properties["pers:interior_orientation"]["visible_area"];
158
+ const sad = picture.properties["pers:interior_orientation"]["sensor_array_dimensions"];
159
+ try {
160
+ res = {
161
+ fullWidth: parseInt(sad[0]),
162
+ fullHeight: parseInt(sad[1]),
163
+ croppedX: parseInt(va[0]),
164
+ croppedY: parseInt(va[1]),
165
+ croppedWidth: parseInt(sad[0]) - parseInt(va[2]) - parseInt(va[0]),
166
+ croppedHeight: parseInt(sad[1]) - parseInt(va[3]) - parseInt(va[1]),
167
+ };
168
+ }
169
+ catch(e) {
170
+ console.warn("Invalid pers:interior_orientation values for cropped panorama "+picture.id);
171
+ }
172
+ }
173
+ }
174
+
175
+ if(!res && picture.properties?.exif) {
176
+ try {
177
+ res = {
178
+ fullWidth: parseInt(picture.properties.exif?.["Xmp.GPano.FullPanoWidthPixels"]),
179
+ fullHeight: parseInt(picture.properties.exif?.["Xmp.GPano.FullPanoHeightPixels"]),
180
+ croppedX: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaLeftPixels"]),
181
+ croppedY: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaTopPixels"]),
182
+ croppedWidth: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaImageWidthPixels"]),
183
+ croppedHeight: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaImageHeightPixels"]),
184
+ };
185
+ }
186
+ catch(e) {
187
+ console.warn("Invalid XMP.GPano values for cropped panorama "+picture.id);
188
+ }
189
+ }
190
+
191
+ // Check if crop is really necessary
192
+ if(res) {
193
+ res = Object.fromEntries(Object.entries(res || {}).filter(e => !isNaN(e[1])));
194
+ if(res.fullWidth == res.croppedWidth && res.fullHeight == res.croppedHeight) {
195
+ res = {};
196
+ }
197
+ }
198
+
199
+ return res || {};
200
+ }
201
+
202
+ /**
203
+ * Compare function to retrieve most appropriate picture in a single direction.
204
+ *
205
+ * @param {number[]} picPos The picture [x,y] position
206
+ * @returns {function} A compare function for sorting
207
+ * @private
208
+ */
209
+ export function sortPicturesInDirection(picPos) {
210
+ return (a,b) => {
211
+ // Two prev/next links = no sort
212
+ if(a.rel != "related" && b.rel != "related") { return 0; }
213
+ // First is prev/next link = goes first
214
+ else if(a.rel != "related") { return -1; }
215
+ // Second is prev/next link = goes first
216
+ else if(b.rel != "related") { return 1; }
217
+ // Two related links same day = nearest goes first
218
+ else if(a.date == b.date) { return getDistance(picPos, a.geometry.coordinates) - getDistance(picPos, b.geometry.coordinates); }
219
+ // Two related links at different day = recent goes first
220
+ else { return b.date.localeCompare(a.date); }
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Generates the navbar caption based on a single picture metadata
226
+ *
227
+ * @param {object} metadata The picture metadata
228
+ * @param {object} t The labels translations container
229
+ * @returns {object} Normalized object with user name, licence and date
230
+ * @private
231
+ */
232
+ export function getNodeCaption(metadata, t) {
233
+ const caption = {};
234
+
235
+ // Timestamp
236
+ if(metadata?.properties?.datetimetz) {
237
+ caption.date = new Date(metadata.properties.datetimetz);
238
+ const timeZoneMatch = metadata.properties.datetimetz.match(/([+-]\d{2}):(\d{2})$|Z$/);
239
+ if (timeZoneMatch) {
240
+ if (timeZoneMatch[0] === "Z") {
241
+ caption.tz = "UTC";
242
+ } else {
243
+ caption.tz = timeZoneMatch[0];
244
+ }
245
+ }
246
+ }
247
+ else if(metadata?.properties?.datetime) {
248
+ caption.date = new Date(metadata.properties.datetime);
249
+ }
250
+
251
+ // Producer
252
+ if(metadata?.providers) {
253
+ const producerRoles = metadata?.providers?.filter(el => el?.roles?.includes("producer"));
254
+ if(producerRoles?.length >= 0) {
255
+ // Avoid duplicates between account name and picture author
256
+ const producersDeduped = {};
257
+ producerRoles.map(p => p.name).forEach(p => {
258
+ const pmin = p.toLowerCase().replace(/\s/g, "");
259
+ if(producersDeduped[pmin]) { producersDeduped[pmin].push(p); }
260
+ else { producersDeduped[pmin] = [p];}
261
+ });
262
+
263
+ // Keep best looking name for each
264
+ caption.producer = [];
265
+ Object.values(producersDeduped).forEach(pv => {
266
+ const deflt = pv[0];
267
+ const better = pv.find(v => v.toLowerCase() != v);
268
+ caption.producer.push(better || deflt);
269
+ });
270
+ }
271
+ }
272
+
273
+ // License
274
+ if(metadata?.properties?.license) {
275
+ caption.license = metadata.properties.license;
276
+ // Look for URL to license
277
+ if(metadata?.links) {
278
+ const licenseLink = metadata.links.find(l => l?.rel === "license");
279
+ if(licenseLink) {
280
+ caption.license = `<a href="${licenseLink.href}" title="${t.pnx.metadata_general_license_link}" target="_blank">${caption.license}</a>`;
281
+ }
282
+ }
283
+ }
284
+
285
+ return caption;
286
+ }
287
+
288
+ /**
289
+ * Transforms a GeoJSON feature from the STAC API into a PSV node.
290
+ *
291
+ * @param {object} f The API GeoJSON feature
292
+ * @param {object} t The labels translations container
293
+ * @param {boolean} [fastInternet] True if Internet speed is high enough for loading HD flat pictures
294
+ * @param {function} [customLinkFilter] A function checking if a STAC link is acceptable to use for picture navigation
295
+ * @return {object} A PSV node
296
+ * @private
297
+ */
298
+ export function apiFeatureToPSVNode(f, t, fastInternet=false, customLinkFilter=null) {
299
+ const isHorizontalFovDefined = f.properties?.["pers:interior_orientation"]?.["field_of_view"] != null;
300
+ let horizontalFov = isHorizontalFovDefined ? parseInt(f.properties["pers:interior_orientation"]["field_of_view"]) : 70;
301
+ const is360 = horizontalFov === 360;
302
+
303
+ const hdUrl = (Object.values(f.assets).find(a => a?.roles?.includes("data")) || {}).href;
304
+ const matrix = f?.properties?.["tiles:tile_matrix_sets"]?.geovisio;
305
+ const prev = f.links?.find?.(l => l?.rel === "prev" && l?.type === "application/geo+json");
306
+ const next = f.links?.find?.(l => l?.rel === "next" && l?.type === "application/geo+json");
307
+ const baseUrlWebp = Object.values(f.assets).find(a => a.roles?.includes("visual") && a.type === "image/webp");
308
+ const baseUrlJpeg = Object.values(f.assets).find(a => a.roles?.includes("visual") && a.type === "image/jpeg");
309
+ const baseUrl = (baseUrlWebp || baseUrlJpeg).href;
310
+ const thumbUrl = (Object.values(f.assets).find(a => a.roles?.includes("thumbnail") && a.type === "image/jpeg"))?.href;
311
+ const tileUrl = f?.asset_templates?.tiles_webp || f?.asset_templates?.tiles;
312
+ const croppedPanoData = getCroppedPanoData(f);
313
+ const origInstance = f.links?.find?.(l => l?.rel === "via" && l?.type === "application/json");
314
+
315
+ let panorama;
316
+
317
+ // Cropped panorama
318
+ if(Object.keys(croppedPanoData).length > 0) {
319
+ panorama = {
320
+ baseUrl: fastInternet ? hdUrl : baseUrl,
321
+ origBaseUrl: fastInternet ? hdUrl : baseUrl,
322
+ hdUrl,
323
+ thumbUrl,
324
+ basePanoData: croppedPanoData,
325
+ // This is only to mock loading of tiles (which are not available for flat pictures)
326
+ cols: 2, rows: 1, width: 2, tileUrl: () => null
327
+ };
328
+ }
329
+ // 360°
330
+ else if(is360 && matrix) {
331
+ panorama = {
332
+ baseUrl,
333
+ origBaseUrl: baseUrl,
334
+ basePanoData: (img) => ({
335
+ fullWidth: img.width,
336
+ fullHeight: img.height,
337
+ }),
338
+ hdUrl,
339
+ thumbUrl,
340
+ cols: matrix && matrix.tileMatrix[0].matrixWidth,
341
+ rows: matrix && matrix.tileMatrix[0].matrixHeight,
342
+ width: matrix && (matrix.tileMatrix[0].matrixWidth * matrix.tileMatrix[0].tileWidth),
343
+ tileUrl: matrix && ((col, row) => tileUrl.href.replace(/\{TileCol\}/g, col).replace(/\{TileRow\}/g, row))
344
+ };
345
+ }
346
+ // Flat pictures: shown only using a cropped base panorama
347
+ else {
348
+ panorama = {
349
+ baseUrl: fastInternet ? hdUrl : baseUrl,
350
+ origBaseUrl: fastInternet ? hdUrl : baseUrl,
351
+ hdUrl,
352
+ thumbUrl,
353
+ basePanoData: (img) => {
354
+ if (img.width < img.height && !isHorizontalFovDefined) {
355
+ horizontalFov = 35;
356
+ }
357
+ const verticalFov = horizontalFov * img.height / img.width;
358
+ const panoWidth = img.width * 360 / horizontalFov;
359
+ const panoHeight = img.height * 180 / verticalFov;
360
+
361
+ return {
362
+ fullWidth: panoWidth,
363
+ fullHeight: panoHeight,
364
+ croppedWidth: img.width,
365
+ croppedHeight: img.height,
366
+ croppedX: (panoWidth - img.width) / 2,
367
+ croppedY: (panoHeight - img.height) / 2,
368
+ };
369
+ },
370
+ // This is only to mock loading of tiles (which are not available for flat pictures)
371
+ cols: 2, rows: 1, width: 2, tileUrl: () => null
372
+ };
373
+ }
374
+
375
+ const node = {
376
+ id: f.id,
377
+ caption: getNodeCaption(f, t),
378
+ panorama,
379
+ links: filterRelatedPicsLinks(f, customLinkFilter),
380
+ gps: f.geometry.coordinates,
381
+ sequence: {
382
+ id: f.collection,
383
+ nextPic: next ? next.id : undefined,
384
+ prevPic: prev ? prev.id : undefined
385
+ },
386
+ sphereCorrection: getSphereCorrection(f),
387
+ horizontalFov,
388
+ origInstance,
389
+ properties: f.properties,
390
+ };
391
+
392
+ return node;
393
+ }
394
+
395
+ /**
396
+ * Filter surrounding pictures links to avoid too much arrows on viewer.
397
+ * @private
398
+ */
399
+ export function filterRelatedPicsLinks(metadata, customFilter = null) {
400
+ const picLinks = metadata.links
401
+ .filter(l => ["next", "prev", "related"].includes(l?.rel) && l?.type === "application/geo+json")
402
+ .filter(l => customFilter ? customFilter(l) : true)
403
+ .map(l => {
404
+ if(l.datetime) {
405
+ l.date = l.datetime.split("T")[0];
406
+ }
407
+ return l;
408
+ });
409
+ const picPos = metadata.geometry.coordinates;
410
+
411
+ // Filter to keep a single link per direction, in same sequence or most recent one
412
+ const filteredLinks = [];
413
+ const picSurroundings = { "N": [], "ENE": [], "ESE": [], "S": [], "WSW": [], "WNW": [] };
414
+
415
+ for(let picLink of picLinks) {
416
+ const a = getSimplifiedAngle(picPos, picLink.geometry.coordinates);
417
+ picSurroundings[a].push(picLink);
418
+ }
419
+
420
+ for(let direction in picSurroundings) {
421
+ const picsInDirection = picSurroundings[direction];
422
+ if(picsInDirection.length == 0) { continue; }
423
+ picsInDirection.sort(sortPicturesInDirection(picPos));
424
+ filteredLinks.push(picsInDirection.shift());
425
+ }
426
+
427
+ let arrowStyle = l => l.rel === "related" ? {
428
+ element: getArrow(ArrowTurn),
429
+ size: { width: 64*2/3, height: 192*2/3 }
430
+ } : {
431
+ element: getArrow(ArrowTriangle),
432
+ size: { width: 75, height: 75 }
433
+ };
434
+
435
+ const rectifiedYaw = - (metadata.properties?.["view:azimuth"] || 0) * (Math.PI / 180);
436
+ return filteredLinks.map(l => ({
437
+ nodeId: l.id,
438
+ gps: l.geometry.coordinates,
439
+ arrowStyle: arrowStyle(l),
440
+ linkOffset: { yaw: rectifiedYaw }
441
+ }));
442
+ }