@panoramax/web-viewer 3.2.3-develop-d7e5a16d → 3.2.3-develop-6257391e

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 (221) hide show
  1. package/.gitlab-ci.yml +3 -0
  2. package/CHANGELOG.md +19 -0
  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 +2 -2
  7. package/build/index.css.map +1 -1
  8. package/build/index.html +1 -1
  9. package/build/index.js +1682 -5
  10. package/build/index.js.map +1 -1
  11. package/build/map.html +1 -1
  12. package/build/viewer.html +10 -1
  13. package/build/widgets.html +1 -0
  14. package/config/jest/mocks.js +172 -0
  15. package/config/paths.js +1 -0
  16. package/config/webpack.config.js +26 -0
  17. package/docs/03_URL_settings.md +3 -11
  18. package/docs/05_Compatibility.md +59 -76
  19. package/docs/09_Develop.md +30 -11
  20. package/docs/90_Releases.md +2 -2
  21. package/docs/images/class_diagram.drawio +28 -28
  22. package/docs/images/class_diagram.jpg +0 -0
  23. package/docs/index.md +112 -0
  24. package/docs/reference/components/core/Basic.md +153 -0
  25. package/docs/reference/components/core/CoverageMap.md +160 -0
  26. package/docs/reference/components/core/Editor.md +172 -0
  27. package/docs/reference/components/core/Viewer.md +288 -0
  28. package/docs/reference/components/layout/CorneredGrid.md +29 -0
  29. package/docs/reference/components/layout/Mini.md +45 -0
  30. package/docs/reference/components/menus/MapBackground.md +32 -0
  31. package/docs/reference/components/menus/MapFilters.md +15 -0
  32. package/docs/reference/components/menus/MapLayers.md +15 -0
  33. package/docs/reference/components/menus/MapLegend.md +15 -0
  34. package/docs/reference/components/menus/PictureLegend.md +15 -0
  35. package/docs/reference/components/menus/PictureMetadata.md +15 -0
  36. package/docs/reference/components/menus/PlayerOptions.md +15 -0
  37. package/docs/reference/components/menus/QualityScoreDoc.md +15 -0
  38. package/docs/reference/components/menus/ReportForm.md +15 -0
  39. package/docs/reference/components/menus/ShareMenu.md +15 -0
  40. package/docs/reference/components/ui/Button.md +39 -0
  41. package/docs/reference/components/ui/ButtonGroup.md +36 -0
  42. package/docs/reference/components/ui/CopyButton.md +35 -0
  43. package/docs/reference/components/ui/Grade.md +32 -0
  44. package/docs/reference/components/ui/LinkButton.md +44 -0
  45. package/docs/reference/components/ui/Loader.md +54 -0
  46. package/docs/reference/components/ui/Map.md +214 -0
  47. package/docs/reference/components/ui/MapMore.md +233 -0
  48. package/docs/reference/components/ui/Photo.md +369 -0
  49. package/docs/reference/components/ui/Popup.md +56 -0
  50. package/docs/reference/components/ui/QualityScore.md +45 -0
  51. package/docs/reference/components/ui/SearchBar.md +63 -0
  52. package/docs/reference/components/ui/TogglableGroup.md +39 -0
  53. package/docs/reference/components/ui/widgets/GeoSearch.md +32 -0
  54. package/docs/reference/components/ui/widgets/Legend.md +32 -0
  55. package/docs/reference/components/ui/widgets/MapFiltersButton.md +33 -0
  56. package/docs/reference/components/ui/widgets/MapLayersButton.md +15 -0
  57. package/docs/reference/components/ui/widgets/Player.md +32 -0
  58. package/docs/reference/components/ui/widgets/Share.md +15 -0
  59. package/docs/reference/components/ui/widgets/Zoom.md +15 -0
  60. package/docs/reference/utils/API.md +311 -0
  61. package/docs/reference/utils/InitParameters.md +67 -0
  62. package/docs/reference/utils/URLHandler.md +102 -0
  63. package/docs/reference.md +73 -0
  64. package/docs/shortcuts.md +11 -0
  65. package/docs/tutorials/aerial_imagery.md +19 -0
  66. package/docs/tutorials/authentication.md +10 -0
  67. package/docs/tutorials/custom_widgets.md +64 -0
  68. package/docs/tutorials/map_style.md +27 -0
  69. package/docs/tutorials/migrate_v4.md +122 -0
  70. package/docs/tutorials/synced_coverage.md +42 -0
  71. package/mkdocs.yml +60 -5
  72. package/package.json +10 -7
  73. package/public/editor.html +21 -29
  74. package/public/index.html +3 -3
  75. package/public/map.html +19 -18
  76. package/public/viewer.html +18 -24
  77. package/public/widgets.html +265 -0
  78. package/scripts/doc.js +77 -0
  79. package/src/components/core/Basic.css +44 -0
  80. package/src/components/core/Basic.js +258 -0
  81. package/src/components/core/CoverageMap.css +9 -0
  82. package/src/components/core/CoverageMap.js +105 -0
  83. package/src/components/core/Editor.css +23 -0
  84. package/src/components/core/Editor.js +354 -0
  85. package/src/components/core/Viewer.css +109 -0
  86. package/src/components/core/Viewer.js +707 -0
  87. package/src/components/core/index.js +11 -0
  88. package/src/components/index.js +13 -0
  89. package/src/components/layout/CorneredGrid.js +109 -0
  90. package/src/components/layout/Mini.js +117 -0
  91. package/src/components/layout/index.js +7 -0
  92. package/src/components/menus/MapBackground.js +106 -0
  93. package/src/components/menus/MapFilters.js +386 -0
  94. package/src/components/menus/MapLayers.js +143 -0
  95. package/src/components/menus/MapLegend.js +54 -0
  96. package/src/components/menus/PictureLegend.js +103 -0
  97. package/src/components/menus/PictureMetadata.js +188 -0
  98. package/src/components/menus/PlayerOptions.js +96 -0
  99. package/src/components/menus/QualityScoreDoc.js +36 -0
  100. package/src/components/menus/ReportForm.js +133 -0
  101. package/src/components/menus/Share.js +228 -0
  102. package/src/components/menus/index.js +15 -0
  103. package/src/components/styles.js +365 -0
  104. package/src/components/ui/Button.js +75 -0
  105. package/src/components/ui/ButtonGroup.css +49 -0
  106. package/src/components/ui/ButtonGroup.js +68 -0
  107. package/src/components/ui/CopyButton.js +71 -0
  108. package/src/components/ui/Grade.js +54 -0
  109. package/src/components/ui/LinkButton.js +68 -0
  110. package/src/components/ui/Loader.js +188 -0
  111. package/src/components/{Map.css → ui/Map.css} +5 -17
  112. package/src/components/{Map.js → ui/Map.js} +114 -138
  113. package/src/components/ui/MapMore.js +324 -0
  114. package/src/components/{Photo.css → ui/Photo.css} +6 -6
  115. package/src/components/{Photo.js → ui/Photo.js} +279 -90
  116. package/src/components/ui/Popup.js +145 -0
  117. package/src/components/ui/QualityScore.js +152 -0
  118. package/src/components/ui/SearchBar.js +363 -0
  119. package/src/components/ui/TogglableGroup.js +162 -0
  120. package/src/components/ui/index.js +20 -0
  121. package/src/components/ui/widgets/GeoSearch.css +21 -0
  122. package/src/components/ui/widgets/GeoSearch.js +139 -0
  123. package/src/components/ui/widgets/Legend.js +51 -0
  124. package/src/components/ui/widgets/MapFiltersButton.js +104 -0
  125. package/src/components/ui/widgets/MapLayersButton.js +79 -0
  126. package/src/components/ui/widgets/Player.css +7 -0
  127. package/src/components/ui/widgets/Player.js +148 -0
  128. package/src/components/ui/widgets/Share.js +30 -0
  129. package/src/components/ui/widgets/Zoom.js +82 -0
  130. package/src/components/ui/widgets/index.js +12 -0
  131. package/src/img/panoramax.svg +13 -0
  132. package/src/img/switch_big.svg +20 -10
  133. package/src/index.js +6 -9
  134. package/src/translations/da.json +1 -1
  135. package/src/translations/de.json +1 -1
  136. package/src/translations/en.json +5 -3
  137. package/src/translations/eo.json +1 -1
  138. package/src/translations/es.json +1 -1
  139. package/src/translations/fr.json +5 -3
  140. package/src/translations/hu.json +1 -1
  141. package/src/translations/it.json +1 -1
  142. package/src/translations/ja.json +1 -1
  143. package/src/translations/nl.json +1 -1
  144. package/src/translations/pl.json +1 -1
  145. package/src/translations/sv.json +1 -1
  146. package/src/translations/zh_Hant.json +1 -1
  147. package/src/utils/API.js +74 -42
  148. package/src/utils/InitParameters.js +354 -0
  149. package/src/utils/URLHandler.js +364 -0
  150. package/src/utils/geocoder.js +116 -0
  151. package/src/utils/{I18n.js → i18n.js} +3 -1
  152. package/src/utils/index.js +11 -0
  153. package/src/utils/{Map.js → map.js} +216 -80
  154. package/src/utils/picture.js +433 -0
  155. package/src/utils/utils.js +315 -0
  156. package/src/utils/widgets.js +93 -0
  157. package/tests/components/ui/CopyButton.test.js +52 -0
  158. package/tests/components/ui/Loader.test.js +54 -0
  159. package/tests/components/{Map.test.js → ui/Map.test.js} +19 -61
  160. package/tests/components/{Photo.test.js → ui/Photo.test.js} +89 -57
  161. package/tests/components/ui/Popup.test.js +24 -0
  162. package/tests/components/ui/QualityScore.test.js +17 -0
  163. package/tests/components/ui/SearchBar.test.js +107 -0
  164. package/tests/components/ui/__snapshots__/CopyButton.test.js.snap +34 -0
  165. package/tests/components/ui/__snapshots__/Loader.test.js.snap +56 -0
  166. package/tests/components/{__snapshots__ → ui/__snapshots__}/Map.test.js.snap +11 -38
  167. package/tests/components/{__snapshots__ → ui/__snapshots__}/Photo.test.js.snap +57 -4
  168. package/tests/components/ui/__snapshots__/Popup.test.js.snap +29 -0
  169. package/tests/components/ui/__snapshots__/QualityScore.test.js.snap +11 -0
  170. package/tests/components/ui/__snapshots__/SearchBar.test.js.snap +65 -0
  171. package/tests/utils/API.test.js +1 -14
  172. package/tests/utils/InitParameters.test.js +485 -0
  173. package/tests/utils/URLHandler.test.js +350 -0
  174. package/tests/utils/__snapshots__/URLHandler.test.js.snap +21 -0
  175. package/tests/utils/__snapshots__/picture.test.js.snap +315 -0
  176. package/tests/utils/__snapshots__/widgets.test.js.snap +19 -0
  177. package/tests/utils/geocoder.test.js +37 -0
  178. package/tests/utils/{I18n.test.js → i18n.test.js} +1 -1
  179. package/tests/utils/map.test.js +67 -0
  180. package/tests/utils/picture.test.js +745 -0
  181. package/tests/utils/utils.test.js +288 -0
  182. package/tests/utils/widgets.test.js +90 -0
  183. package/docs/01_Start.md +0 -149
  184. package/docs/02_Usage.md +0 -831
  185. package/docs/04_Advanced_examples.md +0 -216
  186. package/src/Editor.css +0 -37
  187. package/src/Editor.js +0 -361
  188. package/src/StandaloneMap.js +0 -114
  189. package/src/Viewer.css +0 -203
  190. package/src/Viewer.js +0 -1246
  191. package/src/components/CoreView.css +0 -70
  192. package/src/components/CoreView.js +0 -175
  193. package/src/components/Loader.css +0 -74
  194. package/src/components/Loader.js +0 -120
  195. package/src/utils/Exif.js +0 -193
  196. package/src/utils/Utils.js +0 -631
  197. package/src/utils/Widgets.js +0 -562
  198. package/src/viewer/URLHash.js +0 -469
  199. package/src/viewer/Widgets.css +0 -880
  200. package/src/viewer/Widgets.js +0 -1470
  201. package/tests/Editor.test.js +0 -126
  202. package/tests/StandaloneMap.test.js +0 -45
  203. package/tests/Viewer.test.js +0 -366
  204. package/tests/__snapshots__/Editor.test.js.snap +0 -298
  205. package/tests/__snapshots__/StandaloneMap.test.js.snap +0 -30
  206. package/tests/__snapshots__/Viewer.test.js.snap +0 -195
  207. package/tests/components/CoreView.test.js +0 -92
  208. package/tests/components/Loader.test.js +0 -38
  209. package/tests/components/__snapshots__/Loader.test.js.snap +0 -15
  210. package/tests/utils/Exif.test.js +0 -124
  211. package/tests/utils/Map.test.js +0 -113
  212. package/tests/utils/Utils.test.js +0 -300
  213. package/tests/utils/Widgets.test.js +0 -107
  214. package/tests/utils/__snapshots__/Exif.test.js.snap +0 -43
  215. package/tests/utils/__snapshots__/Utils.test.js.snap +0 -41
  216. package/tests/utils/__snapshots__/Widgets.test.js.snap +0 -44
  217. package/tests/viewer/URLHash.test.js +0 -559
  218. package/tests/viewer/Widgets.test.js +0 -127
  219. package/tests/viewer/__snapshots__/URLHash.test.js.snap +0 -108
  220. package/tests/viewer/__snapshots__/Widgets.test.js.snap +0 -403
  221. /package/tests/utils/__snapshots__/{Map.test.js.snap → geocoder.test.js.snap} +0 -0
@@ -1,631 +0,0 @@
1
- import { getSphereCorrection, getCroppedPanoData } from "./Exif";
2
-
3
- import ArrowTriangleSVG from "../img/arrow_triangle.svg";
4
- import ArrowTurnSVG from "../img/arrow_turn.svg";
5
-
6
- export const BASE_PANORAMA_ID = "geovisio-fake-id-0";
7
-
8
- export const COLORS = {
9
- BASE: "#FF6F00",
10
- SELECTED: "#1E88E5",
11
- HIDDEN: "#34495E",
12
- NEXT: "#ffab40",
13
-
14
- QUALI_1: "#00695C", // 360
15
- QUALI_2: "#fd8d3c", // flat
16
-
17
- PALETTE_1: "#fecc5c", // Oldest
18
- PALETTE_2: "#fd8d3c",
19
- PALETTE_3: "#f03b20",
20
- PALETTE_4: "#bd0026" // Newest
21
- };
22
-
23
- export const COLORS_HEX = Object.fromEntries(Object.entries(COLORS).map(e => {
24
- e[1] = parseInt(e[1].slice(1), 16);
25
- return e;
26
- }));
27
-
28
- export const QUALITYSCORE_VALUES = [
29
- { color: "#007f4e", label: "A" },
30
- { color: "#72b043", label: "B" },
31
- { color: "#b5be2f", label: "C" },
32
- { color: "#f8cc1b", label: "D" },
33
- { color: "#f6a020", label: "E" },
34
- ];
35
-
36
- export const QUALITYSCORE_RES_FLAT_VALUES = [1, 10, 2, 15, 3, 30, 4]; // Grade, < Px/FOV value
37
- export const QUALITYSCORE_RES_360_VALUES = [3, 15, 4, 30, 5]; // Grade, < Px/FOV value
38
- export const QUALITYSCORE_GPS_VALUES = [5, 1.01, 4, 2.01, 3, 5.01, 2, 10.01, 1]; // Grade, < Meters value
39
- export const QUALITYSCORE_POND_RES = 4/5;
40
- export const QUALITYSCORE_POND_GPS = 1/5;
41
-
42
- const ArrowTriangle = svgToPSVLink(ArrowTriangleSVG, "white");
43
- const ArrowTurn = svgToPSVLink(ArrowTurnSVG, COLORS.NEXT);
44
-
45
-
46
- /**
47
- * Find the grade associated to an input Quality Score definition.
48
- * @param {number[]} ranges The QUALITYSCORE_*_VALUES definition
49
- * @param {number} value The picture value
50
- * @return {number} The corresponding grade (1 to 5, or null if missing)
51
- * @private
52
- */
53
- export function getGrade(ranges, value) {
54
- if(value === null || value === undefined || value === "") { return null; }
55
-
56
- // Read each pair from table (grade, reference value)
57
- for(let i = 0; i < ranges.length; i += 2) {
58
- const grade = ranges[i];
59
- const limit = ranges[i+1];
60
-
61
- // Send grade if value is under limit
62
- if (value < limit) { return grade;}
63
- }
64
- // Otherwise, send last grade
65
- return ranges[ranges.length - 1];
66
- }
67
-
68
- /**
69
- * Get cartesian distance between two points
70
- * @param {number[]} from Start [x,y] coordinates
71
- * @param {number[]} to End [x,y] coordinates
72
- * @returns {number} The distance
73
- * @private
74
- */
75
- export function getDistance(from, to) {
76
- const dx = from[0] - to[0];
77
- const dy = from[1] - to[1];
78
- return Math.sqrt(dx*dx + dy*dy);
79
- }
80
-
81
- /**
82
- * Compare function to retrieve most appropriate picture in a single direction.
83
- *
84
- * @param {number[]} picPos The picture [x,y] position
85
- * @returns {function} A compare function for sorting
86
- * @private
87
- */
88
- export function sortPicturesInDirection(picPos) {
89
- return (a,b) => {
90
- // Two prev/next links = no sort
91
- if(a.rel != "related" && b.rel != "related") { return 0; }
92
- // First is prev/next link = goes first
93
- else if(a.rel != "related") { return -1; }
94
- // Second is prev/next link = goes first
95
- else if(b.rel != "related") { return 1; }
96
- // Two related links same day = nearest goes first
97
- else if(a.date == b.date) { return getDistance(picPos, a.geometry.coordinates) - getDistance(picPos, b.geometry.coordinates); }
98
- // Two related links at different day = recent goes first
99
- else { return b.date.localeCompare(a.date); }
100
- };
101
- }
102
-
103
- /**
104
- * Transforms a Base64 SVG string into a DOM img element.
105
- * @param {string} svg The SVG as Base64 string
106
- * @returns {Element} The DOM image element
107
- * @private
108
- */
109
- function svgToPSVLink(svg, fillColor) {
110
- try {
111
- const svgStr = atob(svg.replace(/^data:image\/svg\+xml;base64,/, ""));
112
- const svgXml = (new DOMParser()).parseFromString(svgStr, "image/svg+xml").childNodes[0];
113
- const btn = document.createElement("button");
114
- btn.appendChild(svgXml);
115
- btn.classList.add("gvs-psv-tour-arrows");//"psv-virtual-tour-arrow", "psv-virtual-tour-link");
116
- btn.style.color = fillColor;
117
- return btn;
118
- }
119
- catch(e) {
120
- const img = document.createElement("img");
121
- img.src = svg;
122
- img.alt = "";
123
- return img;
124
- }
125
- }
126
-
127
- /**
128
- * Clones a model PSV link
129
- * @private
130
- */
131
- function getArrow(a) {
132
- const d = a.cloneNode(true);
133
- d.addEventListener("pointerup", () => d.classList.add("gvs-clicked"));
134
- return d;
135
- }
136
-
137
- /**
138
- * Get direction based on angle
139
- * @param {number[]} from Start [x,y] coordinates
140
- * @param {number[]} to End [x,y] coordinates
141
- * @returns {number} The azimuth, from 0 to 360°
142
- * @private
143
- */
144
- export function getAzimuth(from, to) {
145
- return (Math.atan2(to[0] - from[0], to[1] - from[1]) * (180 / Math.PI) + 360) % 360;
146
- }
147
-
148
- /**
149
- * Computes relative heading for a single picture, based on its metadata
150
- * @param {*} m The picture metadata
151
- * @returns {number} The relative heading
152
- * @private
153
- */
154
- export function getRelativeHeading(m) {
155
- if(!m) { throw new Error("No picture selected"); }
156
-
157
- let prevSegDir, nextSegDir;
158
- const currHeading = m.properties["view:azimuth"];
159
-
160
- // Previous picture GPS coordinates
161
- if(m?.sequence?.prevPic) {
162
- const prevLink = m?.links?.find(l => l.nodeId === m.sequence.prevPic);
163
- if(prevLink) {
164
- prevSegDir = (((currHeading - getAzimuth(prevLink.gps, m.gps)) + 180) % 360) - 180;
165
- }
166
- }
167
-
168
- // Next picture GPS coordinates
169
- if(m?.sequence?.nextPic) {
170
- const nextLink = m?.links?.find(l => l.nodeId === m.sequence.nextPic);
171
- if(nextLink) {
172
- nextSegDir = (((currHeading - getAzimuth(m.gps, nextLink.gps)) + 180) % 360) - 180;
173
- }
174
- }
175
-
176
- return prevSegDir !== undefined ? prevSegDir : (nextSegDir !== undefined ? nextSegDir : 0);
177
- }
178
-
179
- /**
180
- * Get direction based on angle
181
- * @param {number[]} from Start [x,y] coordinates
182
- * @param {number[]} to End [x,y] coordinates
183
- * @returns {string} Direction (N/ENE/ESE/S/WSW/WNW)
184
- * @private
185
- */
186
- export function getSimplifiedAngle(from, to) {
187
- const angle = Math.atan2(to[0] - from[0], to[1] - from[1]) * (180 / Math.PI); // -180 to 180°
188
-
189
- // 6 directions version
190
- if (Math.abs(angle) < 30) { return "N"; }
191
- else if (angle >= 30 && angle < 90) { return "ENE"; }
192
- else if (angle >= 90 && angle < 150) { return "ESE"; }
193
- else if (Math.abs(angle) >= 150) { return "S"; }
194
- else if (angle <= -30 && angle > -90) { return "WNW"; }
195
- else if (angle <= -90 && angle > -150) { return "WSW"; }
196
- }
197
-
198
- /**
199
- * Converts result from getPosition or position-updated event into x/y/z coordinates
200
- *
201
- * @param {object} pos pitch/yaw as given by PSV
202
- * @param {number} zoom zoom as given by PSV
203
- * @returns {object} Coordinates as x/y in degrees and zoom as given by PSV
204
- * @private
205
- */
206
- export function positionToXYZ(pos, zoom = undefined) {
207
- const res = {
208
- x: pos.yaw * (180/Math.PI),
209
- y: pos.pitch * (180/Math.PI)
210
- };
211
-
212
- if(zoom !== undefined) { res.z = zoom; }
213
- return res;
214
- }
215
-
216
- /**
217
- * Converts x/y/z coordinates into PSV position (lat/lon/zoom)
218
- *
219
- * @param {number} x The X coordinate (in degrees)
220
- * @param {number} y The Y coordinate (in degrees)
221
- * @param {number} z The zoom level (0-100)
222
- * @returns {object} Position coordinates as yaw/pitch/zoom
223
- * @private
224
- */
225
- export function xyzToPosition(x, y, z) {
226
- return {
227
- yaw: x / (180/Math.PI),
228
- pitch: y / (180/Math.PI),
229
- zoom: z
230
- };
231
- }
232
-
233
- /**
234
- * Generates the navbar caption based on a single picture metadata
235
- *
236
- * @param {object} metadata The picture metadata
237
- * @param {object} t The labels translations container
238
- * @returns {object} Normalized object with user name, licence and date
239
- * @private
240
- */
241
- export function getNodeCaption(metadata, t) {
242
- const caption = {};
243
-
244
- // Timestamp
245
- if(metadata?.properties?.datetimetz) {
246
- caption.date = new Date(metadata.properties.datetimetz);
247
- }
248
- else if(metadata?.properties?.datetime) {
249
- caption.date = new Date(metadata.properties.datetime);
250
- }
251
-
252
- // Producer
253
- if(metadata?.providers) {
254
- const producerRoles = metadata?.providers?.filter(el => el?.roles?.includes("producer"));
255
- if(producerRoles?.length >= 0) {
256
- // Avoid duplicates between account name and picture author
257
- const producersDeduped = {};
258
- producerRoles.map(p => p.name).forEach(p => {
259
- const pmin = p.toLowerCase().replace(/\s/g, "");
260
- if(producersDeduped[pmin]) { producersDeduped[pmin].push(p); }
261
- else { producersDeduped[pmin] = [p];}
262
- });
263
-
264
- // Keep best looking name for each
265
- caption.producer = [];
266
- Object.values(producersDeduped).forEach(pv => {
267
- const deflt = pv[0];
268
- const better = pv.find(v => v.toLowerCase() != v);
269
- caption.producer.push(better || deflt);
270
- });
271
- caption.producer = caption.producer.join(", ");
272
- }
273
- }
274
-
275
- // License
276
- if(metadata?.properties?.license) {
277
- caption.license = metadata.properties.license;
278
- // Look for URL to license
279
- if(metadata?.links) {
280
- const licenseLink = metadata.links.find(l => l?.rel === "license");
281
- if(licenseLink) {
282
- caption.license = `<a href="${licenseLink.href}" title="${t.gvs.metadata_general_license_link}" target="_blank">${caption.license}</a>`;
283
- }
284
- }
285
- }
286
-
287
- return caption;
288
- }
289
-
290
- /**
291
- * Creates links between map and photo elements.
292
- * This enable interactions like click on map showing picture.
293
- *
294
- * @param {CoreView} parent The view containing both Photo and Map elements
295
- * @private
296
- */
297
- export function linkMapAndPhoto(parent) {
298
- // Switched picture
299
- const onPicLoad = e => {
300
- if(!e.detail.picId || e.detail.picId === BASE_PANORAMA_ID) {
301
- parent.map.displayPictureMarker();
302
- if(parent?.isMapWide()) {
303
- parent?.setUnfocusedVisible(false);
304
- }
305
- }
306
- else {
307
- parent.map.displayPictureMarker(e.detail.lon, e.detail.lat, parent.psv.getXY().x);
308
- if(parent?.isMapWide()) {
309
- parent?.setUnfocusedVisible(true);
310
- }
311
- }
312
- };
313
- parent.addEventListener("psv:picture-loading", onPicLoad);
314
- parent.addEventListener("psv:picture-loaded", onPicLoad);
315
-
316
- // Picture view rotated
317
- parent.addEventListener("psv:view-rotated", () => {
318
- let x = parent.psv.getPosition().yaw * (180 / Math.PI);
319
- x += parent.psv.getPictureOriginalHeading();
320
- parent.map._picMarker.setRotation(x);
321
- });
322
-
323
- // Picture preview
324
- parent.addEventListener("psv:picture-preview-started", e => {
325
- // Show marker corresponding to selection
326
- parent.map._picMarkerPreview
327
- .setLngLat(e.detail.coordinates)
328
- .setRotation(e.detail.direction || 0)
329
- .addTo(parent.map);
330
- });
331
-
332
- parent.addEventListener("psv:picture-preview-stopped", () => {
333
- parent.map._picMarkerPreview.remove();
334
- });
335
-
336
- parent.addEventListener("psv:picture-loaded", e => {
337
- if (parent.isWidthSmall() && parent._picPopup && e.detail.picId == parent._picPopup._picId) {
338
- parent._picPopup.remove();
339
- }
340
- });
341
-
342
- // Picture click
343
- parent.addEventListener("map:picture-click", e => {
344
- parent.select(e.detail.seqId, e.detail.picId);
345
- if(!parent.psv._myVTour.state.currentNode && parent?.setFocus) { parent.setFocus("pic"); }
346
- });
347
-
348
- // Sequence click
349
- parent.addEventListener("map:sequence-click", e => {
350
- parent._api.getPicturesAroundCoordinates(
351
- e.detail.coordinates.lat,
352
- e.detail.coordinates.lng,
353
- 1,
354
- 1,
355
- e.detail.seqId
356
- ).then(results => {
357
- if(results?.features?.length > 0) {
358
- parent.select(results.features[0]?.collection, results.features[0].id);
359
- if(!parent.psv.getPictureMetadata() && parent?.setFocus) { parent.setFocus("pic"); }
360
- }
361
- });
362
- });
363
- }
364
-
365
- /**
366
- * Transforms a GeoJSON feature from the STAC API into a PSV node.
367
- *
368
- * @param {object} f The API GeoJSON feature
369
- * @param {object} t The labels translations container
370
- * @param {boolean} [fastInternet] True if Internet speed is high enough for loading HD flat pictures
371
- * @param {function} [customLinkFilter] A function checking if a STAC link is acceptable to use for picture navigation
372
- * @return {object} A PSV node
373
- * @private
374
- */
375
- export function apiFeatureToPSVNode(f, t, fastInternet=false, customLinkFilter=null) {
376
- const isHorizontalFovDefined = f.properties?.["pers:interior_orientation"]?.["field_of_view"] != null;
377
- let horizontalFov = isHorizontalFovDefined ? parseInt(f.properties["pers:interior_orientation"]["field_of_view"]) : 70;
378
- const is360 = horizontalFov === 360;
379
-
380
- const hdUrl = (Object.values(f.assets).find(a => a?.roles?.includes("data")) || {}).href;
381
- const matrix = f?.properties?.["tiles:tile_matrix_sets"]?.geovisio;
382
- const prev = f.links.find(l => l?.rel === "prev" && l?.type === "application/geo+json");
383
- const next = f.links.find(l => l?.rel === "next" && l?.type === "application/geo+json");
384
- const baseUrlWebp = Object.values(f.assets).find(a => a.roles?.includes("visual") && a.type === "image/webp");
385
- const baseUrlJpeg = Object.values(f.assets).find(a => a.roles?.includes("visual") && a.type === "image/jpeg");
386
- const baseUrl = (baseUrlWebp || baseUrlJpeg).href;
387
- const thumbUrl = (Object.values(f.assets).find(a => a.roles?.includes("thumbnail") && a.type === "image/jpeg"))?.href;
388
- const tileUrl = f?.asset_templates?.tiles_webp || f?.asset_templates?.tiles;
389
- const croppedPanoData = getCroppedPanoData(f);
390
-
391
- let panorama;
392
-
393
- // Cropped panorama
394
- if(!tileUrl && Object.keys(croppedPanoData).length > 0) {
395
- panorama = {
396
- baseUrl: fastInternet ? hdUrl : baseUrl,
397
- origBaseUrl: fastInternet ? hdUrl : baseUrl,
398
- hdUrl,
399
- thumbUrl,
400
- basePanoData: croppedPanoData,
401
- // This is only to mock loading of tiles (which are not available for flat pictures)
402
- cols: 2, rows: 1, width: 2, tileUrl: () => null
403
- };
404
- }
405
- // 360°
406
- else if(is360 && matrix) {
407
- panorama = {
408
- baseUrl,
409
- origBaseUrl: baseUrl,
410
- basePanoData: (img) => ({
411
- fullWidth: img.width,
412
- fullHeight: img.height,
413
- }),
414
- hdUrl,
415
- thumbUrl,
416
- cols: matrix && matrix.tileMatrix[0].matrixWidth,
417
- rows: matrix && matrix.tileMatrix[0].matrixHeight,
418
- width: matrix && (matrix.tileMatrix[0].matrixWidth * matrix.tileMatrix[0].tileWidth),
419
- tileUrl: matrix && ((col, row) => tileUrl.href.replace(/\{TileCol\}/g, col).replace(/\{TileRow\}/g, row))
420
- };
421
- }
422
- // Flat pictures: shown only using a cropped base panorama
423
- else {
424
- panorama = {
425
- baseUrl: fastInternet ? hdUrl : baseUrl,
426
- origBaseUrl: fastInternet ? hdUrl : baseUrl,
427
- hdUrl,
428
- thumbUrl,
429
- basePanoData: (img) => {
430
- if (img.width < img.height && !isHorizontalFovDefined) {
431
- horizontalFov = 35;
432
- }
433
- const verticalFov = horizontalFov * img.height / img.width;
434
- const panoWidth = img.width * 360 / horizontalFov;
435
- const panoHeight = img.height * 180 / verticalFov;
436
-
437
- return {
438
- fullWidth: panoWidth,
439
- fullHeight: panoHeight,
440
- croppedWidth: img.width,
441
- croppedHeight: img.height,
442
- croppedX: (panoWidth - img.width) / 2,
443
- croppedY: (panoHeight - img.height) / 2,
444
- };
445
- },
446
- // This is only to mock loading of tiles (which are not available for flat pictures)
447
- cols: 2, rows: 1, width: 2, tileUrl: () => null
448
- };
449
- }
450
-
451
- const node = {
452
- id: f.id,
453
- caption: getNodeCaption(f, t),
454
- panorama,
455
- links: filterRelatedPicsLinks(f, customLinkFilter),
456
- gps: f.geometry.coordinates,
457
- sequence: {
458
- id: f.collection,
459
- nextPic: next ? next.id : undefined,
460
- prevPic: prev ? prev.id : undefined
461
- },
462
- sphereCorrection: getSphereCorrection(f),
463
- horizontalFov,
464
- properties: f.properties,
465
- };
466
-
467
- return node;
468
- }
469
-
470
- /**
471
- * Filter surrounding pictures links to avoid too much arrows on viewer.
472
- * @private
473
- */
474
- export function filterRelatedPicsLinks(metadata, customFilter = null) {
475
- const picLinks = metadata.links
476
- .filter(l => ["next", "prev", "related"].includes(l?.rel) && l?.type === "application/geo+json")
477
- .filter(l => customFilter ? customFilter(l) : true)
478
- .map(l => {
479
- if(l.datetime) {
480
- l.date = l.datetime.split("T")[0];
481
- }
482
- return l;
483
- });
484
- const picPos = metadata.geometry.coordinates;
485
-
486
- // Filter to keep a single link per direction, in same sequence or most recent one
487
- const filteredLinks = [];
488
- const picSurroundings = { "N": [], "ENE": [], "ESE": [], "S": [], "WSW": [], "WNW": [] };
489
-
490
- for(let picLink of picLinks) {
491
- const a = getSimplifiedAngle(picPos, picLink.geometry.coordinates);
492
- picSurroundings[a].push(picLink);
493
- }
494
-
495
- for(let direction in picSurroundings) {
496
- const picsInDirection = picSurroundings[direction];
497
- if(picsInDirection.length == 0) { continue; }
498
- picsInDirection.sort(sortPicturesInDirection(picPos));
499
- filteredLinks.push(picsInDirection.shift());
500
- }
501
-
502
- let arrowStyle = l => l.rel === "related" ? {
503
- element: getArrow(ArrowTurn),
504
- size: { width: 64*2/3, height: 192*2/3 }
505
- } : {
506
- element: getArrow(ArrowTriangle),
507
- size: { width: 75, height: 75 }
508
- };
509
-
510
- const rectifiedYaw = - (metadata.properties?.["view:azimuth"] || 0) * (Math.PI / 180);
511
- return filteredLinks.map(l => ({
512
- nodeId: l.id,
513
- gps: l.geometry.coordinates,
514
- arrowStyle: arrowStyle(l),
515
- linkOffset: { yaw: rectifiedYaw }
516
- }));
517
- }
518
-
519
- /**
520
- * Get the query string for JOSM to load current picture area
521
- * @returns {string} The query string, or null if not available
522
- * @private
523
- */
524
- export function josmBboxParameters(meta) {
525
- if(meta) {
526
- const coords = meta.gps;
527
- const heading = meta?.properties?.["view:azimuth"];
528
- const delta = 0.0002;
529
- const values = {
530
- left: coords[0] - (heading === null || heading >= 180 ? delta : 0),
531
- right: coords[0] + (heading === null || heading <= 180 ? delta : 0),
532
- top: coords[1] + (heading === null || heading <= 90 || heading >= 270 ? delta : 0),
533
- bottom: coords[1] - (heading === null || (heading >= 90 && heading <= 270) ? delta : 0),
534
- changeset_source: "Panoramax"
535
- };
536
- return Object.entries(values).map(e => e.join("=")).join("&");
537
- }
538
- else { return null; }
539
- }
540
-
541
- /**
542
- * Check if code runs in an iframe or in a classic page.
543
- * @returns {boolean} True if running in iframe
544
- * @private
545
- */
546
- export function isInIframe() {
547
- try {
548
- return window.self !== window.top;
549
- } catch(e) {
550
- return true;
551
- }
552
- }
553
-
554
-
555
- const INTERNET_FAST_THRESHOLD = 10; // MBit/s
556
- const INTERNET_FAST_STORAGE = "gvs-internet-fast";
557
- const INTERNET_FAST_TESTFILE = "https://panoramax.openstreetmap.fr/images/05/ca/2c/98/0111-4baf-b6f3-587bb8847d2e.jpg";
558
-
559
- /**
560
- * Check if Internet connection is high-speed or not.
561
- * @returns {Promise} Resolves on true if high-speed.
562
- * @private
563
- */
564
- export function isInternetFast() {
565
- // Check if downlink property is available
566
- try {
567
- const speed = navigator.connection.downlink; // MBit/s
568
- return Promise.resolve(speed >= INTERNET_FAST_THRESHOLD);
569
- }
570
- // Fallback for other browsers
571
- catch(e) {
572
- try {
573
- // Check if test has been done before and stored
574
- const isFast = sessionStorage.getItem(INTERNET_FAST_STORAGE);
575
- if(["true", "false"].includes(isFast)) {
576
- return Promise.resolve(isFast === "true");
577
- }
578
-
579
- // Run download testing
580
- const startTime = (new Date()).getTime();
581
- return fetch(INTERNET_FAST_TESTFILE+"?nocache="+startTime)
582
- .then(async res => [res, await res.blob()])
583
- .then(([res, blob]) => {
584
- const size = parseInt(res.headers.get("Content-Length") || blob.size); // Bytes
585
- const endTime = (new Date()).getTime();
586
- const duration = (endTime - startTime) / 1000; // Transfer time in seconds
587
- const speed = (size * 8 / 1024 / 1024) / duration; // MBits/s
588
- const isFast = speed >= INTERNET_FAST_THRESHOLD;
589
- sessionStorage.setItem(INTERNET_FAST_STORAGE, isFast ? "true" : "false");
590
- return isFast;
591
- })
592
- .catch(e => {
593
- console.warn("Failed to run speedtest", e);
594
- return false;
595
- });
596
- }
597
- // Fallback for browser blocking third-party downloads or sessionStorage
598
- catch(e) {
599
- return Promise.resolve(false);
600
- }
601
- }
602
- }
603
-
604
- /**
605
- * Get a cookie value
606
- * @param {str} name The cookie name
607
- * @returns {str} The cookie value, or null if not found
608
- * @private
609
- */
610
- export function getCookie(name) {
611
- const parts = document.cookie
612
- ?.split(";")
613
- ?.find((row) => row.trimStart().startsWith(`${name}=`))
614
- ?.split("=");
615
- if(!parts) { return undefined; }
616
- parts.shift();
617
- return parts.join("=");
618
- }
619
-
620
- /**
621
- * Checks if an user account exists
622
- * @returns {object} Object like {"id", "name"} or null if no authenticated account
623
- * @private
624
- */
625
- export function getUserAccount() {
626
- const session = getCookie("session");
627
- const user_id = getCookie("user_id");
628
- const user_name = getCookie("user_name");
629
-
630
- return (session && user_id && user_name) ? { id: user_id, name: user_name } : null;
631
- }