@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,324 @@
1
+ export const BASE_PANORAMA_ID = "geovisio-fake-id-0";
2
+
3
+ export const COLORS = {
4
+ BASE: "#FF6F00",
5
+ SELECTED: "#1E88E5",
6
+ HIDDEN: "#34495E",
7
+ NEXT: "#ffab40",
8
+
9
+ QUALI_1: "#00695C", // 360
10
+ QUALI_2: "#fd8d3c", // flat
11
+
12
+ PALETTE_1: "#fecc5c", // Oldest
13
+ PALETTE_2: "#fd8d3c",
14
+ PALETTE_3: "#f03b20",
15
+ PALETTE_4: "#bd0026" // Newest
16
+ };
17
+
18
+ export const COLORS_HEX = Object.fromEntries(Object.entries(COLORS).map(e => {
19
+ e[1] = parseInt(e[1].slice(1), 16);
20
+ return e;
21
+ }));
22
+
23
+ export const QUALITYSCORE_VALUES = [
24
+ { color: "#007f4e", label: "A" },
25
+ { color: "#72b043", label: "B" },
26
+ { color: "#b5be2f", label: "C" },
27
+ { color: "#f8cc1b", label: "D" },
28
+ { color: "#f6a020", label: "E" },
29
+ ];
30
+
31
+ export const QUALITYSCORE_RES_FLAT_VALUES = [1, 10, 2, 15, 3, 30, 4]; // Grade, < Px/FOV value
32
+ export const QUALITYSCORE_RES_360_VALUES = [3, 15, 4, 30, 5]; // Grade, < Px/FOV value
33
+ export const QUALITYSCORE_GPS_VALUES = [5, 1.01, 4, 2.01, 3, 5.01, 2, 10.01, 1]; // Grade, < Meters value
34
+ export const QUALITYSCORE_POND_RES = 4/5;
35
+ export const QUALITYSCORE_POND_GPS = 1/5;
36
+
37
+
38
+ /**
39
+ * Checks if a picture or sequence ID is kinda-null.
40
+ * @param {string|null|undefined} id The ID to check
41
+ * @returns True if null-like
42
+ */
43
+ export function isNullId(id) {
44
+ return [null, undefined, "", BASE_PANORAMA_ID].includes(id);
45
+ }
46
+
47
+ /**
48
+ * Find the grade associated to an input Quality Score definition.
49
+ * @param {number[]} ranges The QUALITYSCORE_*_VALUES definition
50
+ * @param {number} value The picture value
51
+ * @return {number} The corresponding grade (1 to 5, or null if missing)
52
+ * @private
53
+ */
54
+ export function getGrade(ranges, value) {
55
+ if(value === null || value === undefined || value === "") { return null; }
56
+
57
+ // Read each pair from table (grade, reference value)
58
+ for(let i = 0; i < ranges.length; i += 2) {
59
+ const grade = ranges[i];
60
+ const limit = ranges[i+1];
61
+
62
+ // Send grade if value is under limit
63
+ if (value < limit) { return grade;}
64
+ }
65
+ // Otherwise, send last grade
66
+ return ranges[ranges.length - 1];
67
+ }
68
+
69
+ /**
70
+ * Get cartesian distance between two points
71
+ * @param {number[]} from Start [x,y] coordinates
72
+ * @param {number[]} to End [x,y] coordinates
73
+ * @returns {number} The distance
74
+ * @private
75
+ */
76
+ export function getDistance(from, to) {
77
+ const dx = from[0] - to[0];
78
+ const dy = from[1] - to[1];
79
+ return Math.sqrt(dx*dx + dy*dy);
80
+ }
81
+
82
+ /**
83
+ * Transforms a Base64 SVG string into a DOM img element.
84
+ * @param {string} svg The SVG as Base64 string
85
+ * @returns {Element} The DOM image element
86
+ * @private
87
+ */
88
+ export function svgToPSVLink(svg, fillColor) {
89
+ try {
90
+ const svgStr = atob(svg.replace(/^data:image\/svg\+xml;base64,/, ""));
91
+ const svgXml = (new DOMParser()).parseFromString(svgStr, "image/svg+xml").childNodes[0];
92
+ const btn = document.createElement("button");
93
+ btn.appendChild(svgXml);
94
+ btn.classList.add("pnx-psv-tour-arrows", "pnx-print-hidden");
95
+ btn.style.color = fillColor;
96
+ return btn;
97
+ }
98
+ catch(e) {
99
+ const img = document.createElement("img");
100
+ img.src = svg;
101
+ img.alt = "";
102
+ return img;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Clones a model PSV link
108
+ * @private
109
+ */
110
+ export function getArrow(a) {
111
+ const d = a.cloneNode(true);
112
+ d.addEventListener("pointerup", () => d.classList.add("pnx-clicked"));
113
+ return d;
114
+ }
115
+
116
+ /**
117
+ * Get direction based on angle
118
+ * @param {number[]} from Start [x,y] coordinates
119
+ * @param {number[]} to End [x,y] coordinates
120
+ * @returns {number} The azimuth, from 0 to 360°
121
+ * @private
122
+ */
123
+ export function getAzimuth(from, to) {
124
+ return (Math.atan2(to[0] - from[0], to[1] - from[1]) * (180 / Math.PI) + 360) % 360;
125
+ }
126
+
127
+ /**
128
+ * Computes relative heading for a single picture, based on its metadata
129
+ * @param {*} m The picture metadata
130
+ * @returns {number} The relative heading
131
+ * @private
132
+ */
133
+ export function getRelativeHeading(m) {
134
+ if(!m) { throw new Error("No picture selected"); }
135
+
136
+ let prevSegDir, nextSegDir;
137
+ const currHeading = m.properties["view:azimuth"];
138
+
139
+ // Previous picture GPS coordinates
140
+ if(m?.sequence?.prevPic) {
141
+ const prevLink = m?.links?.find(l => l.nodeId === m.sequence.prevPic);
142
+ if(prevLink) {
143
+ prevSegDir = (((currHeading - getAzimuth(prevLink.gps, m.gps)) + 180) % 360) - 180;
144
+ }
145
+ }
146
+
147
+ // Next picture GPS coordinates
148
+ if(m?.sequence?.nextPic) {
149
+ const nextLink = m?.links?.find(l => l.nodeId === m.sequence.nextPic);
150
+ if(nextLink) {
151
+ nextSegDir = (((currHeading - getAzimuth(m.gps, nextLink.gps)) + 180) % 360) - 180;
152
+ }
153
+ }
154
+
155
+ return prevSegDir !== undefined ? prevSegDir : (nextSegDir !== undefined ? nextSegDir : 0);
156
+ }
157
+
158
+ /**
159
+ * Get direction based on angle
160
+ * @param {number[]} from Start [x,y] coordinates
161
+ * @param {number[]} to End [x,y] coordinates
162
+ * @returns {string} Direction (N/ENE/ESE/S/WSW/WNW)
163
+ * @private
164
+ */
165
+ export function getSimplifiedAngle(from, to) {
166
+ const angle = Math.atan2(to[0] - from[0], to[1] - from[1]) * (180 / Math.PI); // -180 to 180°
167
+
168
+ // 6 directions version
169
+ if (Math.abs(angle) < 30) { return "N"; }
170
+ else if (angle >= 30 && angle < 90) { return "ENE"; }
171
+ else if (angle >= 90 && angle < 150) { return "ESE"; }
172
+ else if (Math.abs(angle) >= 150) { return "S"; }
173
+ else if (angle <= -30 && angle > -90) { return "WNW"; }
174
+ else if (angle <= -90 && angle > -150) { return "WSW"; }
175
+ }
176
+
177
+ /**
178
+ * Converts result from getPosition or position-updated event into x/y/z coordinates
179
+ *
180
+ * @param {object} pos pitch/yaw as given by PSV
181
+ * @param {number} zoom zoom as given by PSV
182
+ * @returns {object} Coordinates as x/y in degrees and zoom as given by PSV
183
+ * @private
184
+ */
185
+ export function positionToXYZ(pos, zoom = undefined) {
186
+ const res = {
187
+ x: pos.yaw * (180/Math.PI),
188
+ y: pos.pitch * (180/Math.PI)
189
+ };
190
+
191
+ if(zoom !== undefined) { res.z = zoom; }
192
+ return res;
193
+ }
194
+
195
+ /**
196
+ * Converts x/y/z coordinates into PSV position (lat/lon/zoom)
197
+ *
198
+ * @param {number} x The X coordinate (in degrees)
199
+ * @param {number} y The Y coordinate (in degrees)
200
+ * @param {number} z The zoom level (0-100)
201
+ * @returns {object} Position coordinates as yaw/pitch/zoom
202
+ * @private
203
+ */
204
+ export function xyzToPosition(x, y, z) {
205
+ return {
206
+ yaw: x / (180/Math.PI),
207
+ pitch: y / (180/Math.PI),
208
+ zoom: z
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Get the query string for JOSM to load current picture area
214
+ * @returns {string} The query string, or null if not available
215
+ * @private
216
+ */
217
+ export function josmBboxParameters(meta) {
218
+ if(meta) {
219
+ const coords = meta.gps;
220
+ const heading = meta?.properties?.["view:azimuth"];
221
+ const delta = 0.0002;
222
+ const values = {
223
+ left: coords[0] - (heading === null || heading >= 180 ? delta : 0),
224
+ right: coords[0] + (heading === null || heading <= 180 ? delta : 0),
225
+ top: coords[1] + (heading === null || heading <= 90 || heading >= 270 ? delta : 0),
226
+ bottom: coords[1] - (heading === null || (heading >= 90 && heading <= 270) ? delta : 0),
227
+ changeset_source: "Panoramax"
228
+ };
229
+ return Object.entries(values).map(e => e.join("=")).join("&");
230
+ }
231
+ else { return null; }
232
+ }
233
+
234
+ /**
235
+ * Check if code runs in an iframe or in a classic page.
236
+ * @returns {boolean} True if running in iframe
237
+ * @private
238
+ */
239
+ export function isInIframe() {
240
+ try {
241
+ return window.self !== window.top;
242
+ } catch(e) {
243
+ return true;
244
+ }
245
+ }
246
+
247
+
248
+ const INTERNET_FAST_THRESHOLD = 10; // MBit/s
249
+ const INTERNET_FAST_STORAGE = "pnx-internet-fast";
250
+ const INTERNET_FAST_TESTFILE = "https://panoramax.openstreetmap.fr/images/05/ca/2c/98/0111-4baf-b6f3-587bb8847d2e.jpg";
251
+
252
+ /**
253
+ * Check if Internet connection is high-speed or not.
254
+ * @returns {Promise} Resolves on true if high-speed.
255
+ * @private
256
+ */
257
+ export function isInternetFast() {
258
+ // Check if downlink property is available
259
+ try {
260
+ const speed = navigator.connection.downlink; // MBit/s
261
+ return Promise.resolve(speed >= INTERNET_FAST_THRESHOLD);
262
+ }
263
+ // Fallback for other browsers
264
+ catch(e) {
265
+ try {
266
+ // Check if test has been done before and stored
267
+ const isFast = sessionStorage.getItem(INTERNET_FAST_STORAGE);
268
+ if(["true", "false"].includes(isFast)) {
269
+ return Promise.resolve(isFast === "true");
270
+ }
271
+
272
+ // Run download testing
273
+ const startTime = (new Date()).getTime();
274
+ return fetch(INTERNET_FAST_TESTFILE+"?nocache="+startTime)
275
+ .then(async res => [res, await res.blob()])
276
+ .then(([res, blob]) => {
277
+ const size = parseInt(res.headers.get("Content-Length") || blob.size); // Bytes
278
+ const endTime = (new Date()).getTime();
279
+ const duration = (endTime - startTime) / 1000; // Transfer time in seconds
280
+ const speed = (size * 8 / 1024 / 1024) / duration; // MBits/s
281
+ const isFast = speed >= INTERNET_FAST_THRESHOLD;
282
+ sessionStorage.setItem(INTERNET_FAST_STORAGE, isFast ? "true" : "false");
283
+ return isFast;
284
+ })
285
+ .catch(e => {
286
+ console.warn("Failed to run speedtest", e);
287
+ return false;
288
+ });
289
+ }
290
+ // Fallback for browser blocking third-party downloads or sessionStorage
291
+ catch(e) {
292
+ return Promise.resolve(false);
293
+ }
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Get a cookie value
299
+ * @param {str} name The cookie name
300
+ * @returns {str} The cookie value, or null if not found
301
+ * @private
302
+ */
303
+ export function getCookie(name) {
304
+ const parts = document.cookie
305
+ ?.split(";")
306
+ ?.find((row) => row.trimStart().startsWith(`${name}=`))
307
+ ?.split("=");
308
+ if(!parts) { return undefined; }
309
+ parts.shift();
310
+ return parts.join("=");
311
+ }
312
+
313
+ /**
314
+ * Checks if an user account exists
315
+ * @returns {object} Object like {"id", "name"} or null if no authenticated account
316
+ * @private
317
+ */
318
+ export function getUserAccount() {
319
+ const session = getCookie("session");
320
+ const user_id = getCookie("user_id");
321
+ const user_name = getCookie("user_name");
322
+
323
+ return (session && user_id && user_name) ? { id: user_id, name: user_name } : null;
324
+ }
@@ -0,0 +1,55 @@
1
+ // Every single icon imported separately to reduce bundle size
2
+ import { icon } from "@fortawesome/fontawesome-svg-core";
3
+
4
+
5
+ /**
6
+ * Transform Font Awesome icon definition into HTML element
7
+ * @param {IconDefinition} i The icon to use
8
+ * @param {object} [o] [FontAwesome icon parameters](https://origin.fontawesome.com/docs/apis/javascript/methods#icon-icondefinition-params)
9
+ * @returns {Element} HTML element
10
+ * @private
11
+ */
12
+ export function fa(i, o) {
13
+ return icon(i, o).node[0];
14
+ }
15
+
16
+ /**
17
+ * Create a web component with its initial properties
18
+ * @private
19
+ */
20
+ export function createWebComp(tag, props = {}) {
21
+ const wc = document.createElement(tag);
22
+ Object.entries(props).forEach(([k,v]) => {
23
+ if(k.startsWith("_")) { wc[k] = v; }
24
+ else if(k.startsWith("fn")) { wc[k.substring(2)] = v; }
25
+ else if(k.startsWith("on")) { wc.addEventListener(k.substring(2), v); }
26
+ else if(v) { wc.setAttribute(k, v); }
27
+ });
28
+ return wc;
29
+ }
30
+
31
+ /**
32
+ * Listen to parent events that may lead to a menu closure
33
+ * @private
34
+ */
35
+ export function listenForMenuClosure(me, callback) {
36
+ // Other menu opened
37
+ me._parent?.addEventListener("menu-opened", e => {
38
+ if(e.detail.menu != me) { callback(); }
39
+ });
40
+
41
+ // Map click
42
+ me._parent?.onceMapReady?.().then(() => {
43
+ me._parent.map?.on?.("click", () => callback());
44
+ });
45
+
46
+ // Photo click
47
+ me._parent?.oncePSVReady?.().then(() => {
48
+ me._parent.psv.addEventListener("click", () => callback());
49
+ });
50
+
51
+ // Legend click
52
+ me._parent?.onceReady?.().then(() => {
53
+ me._parent.legend?.addEventListener("click", () => callback());
54
+ });
55
+ }
@@ -0,0 +1,121 @@
1
+ import Basic from "../../../src/components/core/Basic";
2
+ import API from "../../../src/utils/API";
3
+
4
+ jest.mock("../../../src/utils/API");
5
+ jest.mock("../../../src/utils/i18n");
6
+ jest.mock("../../../src/utils/widgets");
7
+ jest.mock("../../../src/utils/utils");
8
+ jest.mock("../../../package.json", () => ({ version: "1.0.0", repository: { url: "https://example.com" } }));
9
+
10
+ let basic;
11
+ global.console = { info: jest.fn(), error: jest.fn(), warn: jest.fn(), log: global.console.log };
12
+
13
+ beforeEach(() => {
14
+ basic = new Basic(true);
15
+ });
16
+
17
+ afterEach(() => {
18
+ jest.clearAllMocks();
19
+ });
20
+
21
+ describe("constructor", () => {
22
+ it("should initialize with default values", () => {
23
+ expect(basic.users).toEqual(["geovisio"]);
24
+ expect(basic.mapstyle).toBeDefined();
25
+ expect(basic.lang).toBeNull();
26
+ expect(basic.endpoint).toBeNull();
27
+ expect(basic.picture).toBeNull();
28
+ expect(basic.sequence).toBeNull();
29
+ });
30
+
31
+ it("should log version info on initialization", () => {
32
+ new Basic(true);
33
+ expect(console.info).toHaveBeenCalledWith(expect.stringContaining("Panoramax Basic - Version 1.0.0"));
34
+ });
35
+
36
+ it("should set up API when endpoint is provided", async () => {
37
+ basic.endpoint = "https://api.example.com";
38
+ basic.attributeChangedCallback("endpoint", null, "https://api.example.com");
39
+ expect(API).toHaveBeenCalledWith("https://api.example.com", expect.any(Object));
40
+ });
41
+ });
42
+
43
+ describe("attributeChangedCallback", () => {
44
+ it("should dispatch select event when picture or sequence changes", () => {
45
+ const eventSpy = jest.spyOn(basic, "dispatchEvent");
46
+ basic.attributeChangedCallback("picture", null, "new-pic-id");
47
+ expect(eventSpy).toHaveBeenCalledWith(expect.any(CustomEvent));
48
+ eventSpy.mockRestore();
49
+ });
50
+ });
51
+
52
+ describe("getClassName", () => {
53
+ it("works", () => {
54
+ expect(basic.getClassName()).toBe("Basic");
55
+ });
56
+ });
57
+
58
+ describe("select", () => {
59
+ it("works", () => {
60
+ basic.select("new-seq-id", "new-pic-id");
61
+ expect(basic.picture).toBe("new-pic-id");
62
+ expect(basic.sequence).toBe("new-seq-id");
63
+ });
64
+ });
65
+
66
+ describe("offsetWidth", () => {
67
+ it("works", () => {
68
+ basic.offsetWidth = 500;
69
+ expect(basic.isWidthSmall()).toBe(true);
70
+ basic.offsetWidth = 600;
71
+ expect(basic.isWidthSmall()).toBe(false);
72
+ });
73
+ });
74
+
75
+ describe("offsetHeight", () => {
76
+ it("works", () => {
77
+ basic.offsetHeight = 300;
78
+ expect(basic.isHeightSmall()).toBe(true);
79
+ basic.offsetHeight = 500;
80
+ expect(basic.isHeightSmall()).toBe(false);
81
+ });
82
+ });
83
+
84
+ describe("getSubComponentsNames", () => {
85
+ it("works", () => {
86
+ expect(basic.getSubComponentsNames()).toEqual(["loader", "api"]);
87
+ });
88
+ });
89
+
90
+ describe("addEventListener", () => {
91
+ it("should add event listener for standard events", () => {
92
+ const listener = jest.fn();
93
+ basic.addEventListener("ready", listener);
94
+ basic.dispatchEvent(new CustomEvent("ready"));
95
+ expect(listener).toHaveBeenCalled();
96
+ });
97
+
98
+ it("should add event listener with options", () => {
99
+ const listener = jest.fn();
100
+ basic.addEventListener("once-event", listener, { once: true });
101
+ basic.dispatchEvent(new CustomEvent("once-event"));
102
+ basic.dispatchEvent(new CustomEvent("once-event"));
103
+ expect(listener).toHaveBeenCalledTimes(1);
104
+ });
105
+
106
+ it("should wait for sub-component to be available", () => {
107
+ const listener = jest.fn();
108
+ global.setTimeout = jest.fn();
109
+ basic.getSubComponentsNames = () => ["loader", "api", "map"];
110
+ basic.map = null;
111
+ basic.addEventListener("map:move", listener);
112
+ expect(global.setTimeout).toHaveBeenCalled();
113
+ });
114
+
115
+ it("adds listener to basic if no sub-component matches prefix", () => {
116
+ const listener = jest.fn();
117
+ basic.addEventListener("unknown:event", listener);
118
+ basic.dispatchEvent(new CustomEvent("unknown:event"));
119
+ expect(listener).toHaveBeenCalled();
120
+ });
121
+ });
@@ -0,0 +1,25 @@
1
+ jest.mock("../../../src/components/core/Basic", () => (
2
+ class Basic extends EventTarget {
3
+ constructor() {
4
+ super();
5
+ this.loader = { setAttribute: jest.fn() };
6
+ this.api = {
7
+ getMapStyle: () => ({}),
8
+ _getMapRequestTransform: () => ({}),
9
+ _endpoints: {},
10
+ };
11
+ this._t = { maplibre: {}, psv: {} };
12
+ }
13
+ isWidthSmall() { return false; }
14
+ isHeightSmall() { return false; }
15
+ getSubComponentsNames() {
16
+ return ["loader", "api"];
17
+ }
18
+ onceAPIReady() {
19
+ return Promise.resolve();
20
+ }
21
+ static GetJSONConverter() {
22
+ return {};
23
+ }
24
+ }
25
+ ));
@@ -0,0 +1,20 @@
1
+ import CoverageMap from "../../../src/components/core/CoverageMap";
2
+ import "./BasicMock";
3
+
4
+ let cm;
5
+ global.console = { info: jest.fn(), error: jest.fn(), warn: jest.fn(), log: global.console.log };
6
+
7
+ beforeEach(() => cm = new CoverageMap());
8
+ afterEach(() => jest.clearAllMocks());
9
+
10
+ describe("getClassName", () => {
11
+ it("works", () => {
12
+ expect(cm.getClassName()).toBe("CoverageMap");
13
+ });
14
+ });
15
+
16
+ describe("getSubComponentsNames", () => {
17
+ it("works", () => {
18
+ expect(cm.getSubComponentsNames()).toEqual(["loader", "api", "map"]);
19
+ });
20
+ });
@@ -0,0 +1,20 @@
1
+ import Editor from "../../../src/components/core/Editor";
2
+ import "./BasicMock";
3
+
4
+ let comp;
5
+ global.console = { info: jest.fn(), error: jest.fn(), warn: jest.fn(), log: global.console.log };
6
+
7
+ beforeEach(() => comp = new Editor());
8
+ afterEach(() => jest.clearAllMocks());
9
+
10
+ describe("getClassName", () => {
11
+ it("works", () => {
12
+ expect(comp.getClassName()).toBe("Editor");
13
+ });
14
+ });
15
+
16
+ describe("getSubComponentsNames", () => {
17
+ it("works", () => {
18
+ expect(comp.getSubComponentsNames()).toEqual(["loader", "api", "map", "psv"]);
19
+ });
20
+ });
@@ -0,0 +1,57 @@
1
+ import PhotoViewer from "../../../src/components/core/PhotoViewer";
2
+ import "./BasicMock";
3
+
4
+ let comp;
5
+ global.console = { info: jest.fn(), error: jest.fn(), warn: jest.fn(), log: global.console.log };
6
+
7
+ beforeEach(() => comp = new PhotoViewer());
8
+ afterEach(() => jest.clearAllMocks());
9
+
10
+
11
+ describe("_initWidgets", () => {
12
+ let initParamsMock;
13
+
14
+ beforeEach(() => {
15
+ comp.grid.appendChild = jest.fn();
16
+
17
+ // Mock the return value of getParentPostInit
18
+ initParamsMock = {
19
+ widgets: "true",
20
+ focus: "pic",
21
+ picture: "somePicture",
22
+ };
23
+ comp._createInitParamsHandler();
24
+ comp._initParams.getParentPostInit = jest.fn();
25
+ comp._initParams.getParentPostInit.mockReturnValue(initParamsMock);
26
+ });
27
+
28
+ it("should not add widgets if widgets is false", () => {
29
+ initParamsMock.widgets = "false";
30
+ comp._initWidgets();
31
+ expect(comp.grid.appendChild).not.toHaveBeenCalled();
32
+ });
33
+
34
+ it("should handle widgets if width is not small", () => {
35
+ comp.isWidthSmall = () => false;
36
+ comp._initWidgets();
37
+ expect(comp.grid.appendChild).toMatchSnapshot();
38
+ });
39
+
40
+ it("should handle widgets if width is small", () => {
41
+ comp.isWidthSmall = () => true;
42
+ comp._initWidgets();
43
+ expect(comp.grid.appendChild).toMatchSnapshot();
44
+ });
45
+ });
46
+
47
+ describe("getClassName", () => {
48
+ it("works", () => {
49
+ expect(comp.getClassName()).toBe("PhotoViewer");
50
+ });
51
+ });
52
+
53
+ describe("getSubComponentsNames", () => {
54
+ it("works", () => {
55
+ expect(comp.getSubComponentsNames()).toEqual(["loader", "api", "psv", "grid", "popup", "urlHandler"]);
56
+ });
57
+ });