@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
@@ -0,0 +1,315 @@
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
+ * Find the grade associated to an input Quality Score definition.
40
+ * @param {number[]} ranges The QUALITYSCORE_*_VALUES definition
41
+ * @param {number} value The picture value
42
+ * @return {number} The corresponding grade (1 to 5, or null if missing)
43
+ * @private
44
+ */
45
+ export function getGrade(ranges, value) {
46
+ if(value === null || value === undefined || value === "") { return null; }
47
+
48
+ // Read each pair from table (grade, reference value)
49
+ for(let i = 0; i < ranges.length; i += 2) {
50
+ const grade = ranges[i];
51
+ const limit = ranges[i+1];
52
+
53
+ // Send grade if value is under limit
54
+ if (value < limit) { return grade;}
55
+ }
56
+ // Otherwise, send last grade
57
+ return ranges[ranges.length - 1];
58
+ }
59
+
60
+ /**
61
+ * Get cartesian distance between two points
62
+ * @param {number[]} from Start [x,y] coordinates
63
+ * @param {number[]} to End [x,y] coordinates
64
+ * @returns {number} The distance
65
+ * @private
66
+ */
67
+ export function getDistance(from, to) {
68
+ const dx = from[0] - to[0];
69
+ const dy = from[1] - to[1];
70
+ return Math.sqrt(dx*dx + dy*dy);
71
+ }
72
+
73
+ /**
74
+ * Transforms a Base64 SVG string into a DOM img element.
75
+ * @param {string} svg The SVG as Base64 string
76
+ * @returns {Element} The DOM image element
77
+ * @private
78
+ */
79
+ export function svgToPSVLink(svg, fillColor) {
80
+ try {
81
+ const svgStr = atob(svg.replace(/^data:image\/svg\+xml;base64,/, ""));
82
+ const svgXml = (new DOMParser()).parseFromString(svgStr, "image/svg+xml").childNodes[0];
83
+ const btn = document.createElement("button");
84
+ btn.appendChild(svgXml);
85
+ btn.classList.add("pnx-psv-tour-arrows", "pnx-print-hidden");
86
+ btn.style.color = fillColor;
87
+ return btn;
88
+ }
89
+ catch(e) {
90
+ const img = document.createElement("img");
91
+ img.src = svg;
92
+ img.alt = "";
93
+ return img;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Clones a model PSV link
99
+ * @private
100
+ */
101
+ export function getArrow(a) {
102
+ const d = a.cloneNode(true);
103
+ d.addEventListener("pointerup", () => d.classList.add("pnx-clicked"));
104
+ return d;
105
+ }
106
+
107
+ /**
108
+ * Get direction based on angle
109
+ * @param {number[]} from Start [x,y] coordinates
110
+ * @param {number[]} to End [x,y] coordinates
111
+ * @returns {number} The azimuth, from 0 to 360°
112
+ * @private
113
+ */
114
+ export function getAzimuth(from, to) {
115
+ return (Math.atan2(to[0] - from[0], to[1] - from[1]) * (180 / Math.PI) + 360) % 360;
116
+ }
117
+
118
+ /**
119
+ * Computes relative heading for a single picture, based on its metadata
120
+ * @param {*} m The picture metadata
121
+ * @returns {number} The relative heading
122
+ * @private
123
+ */
124
+ export function getRelativeHeading(m) {
125
+ if(!m) { throw new Error("No picture selected"); }
126
+
127
+ let prevSegDir, nextSegDir;
128
+ const currHeading = m.properties["view:azimuth"];
129
+
130
+ // Previous picture GPS coordinates
131
+ if(m?.sequence?.prevPic) {
132
+ const prevLink = m?.links?.find(l => l.nodeId === m.sequence.prevPic);
133
+ if(prevLink) {
134
+ prevSegDir = (((currHeading - getAzimuth(prevLink.gps, m.gps)) + 180) % 360) - 180;
135
+ }
136
+ }
137
+
138
+ // Next picture GPS coordinates
139
+ if(m?.sequence?.nextPic) {
140
+ const nextLink = m?.links?.find(l => l.nodeId === m.sequence.nextPic);
141
+ if(nextLink) {
142
+ nextSegDir = (((currHeading - getAzimuth(m.gps, nextLink.gps)) + 180) % 360) - 180;
143
+ }
144
+ }
145
+
146
+ return prevSegDir !== undefined ? prevSegDir : (nextSegDir !== undefined ? nextSegDir : 0);
147
+ }
148
+
149
+ /**
150
+ * Get direction based on angle
151
+ * @param {number[]} from Start [x,y] coordinates
152
+ * @param {number[]} to End [x,y] coordinates
153
+ * @returns {string} Direction (N/ENE/ESE/S/WSW/WNW)
154
+ * @private
155
+ */
156
+ export function getSimplifiedAngle(from, to) {
157
+ const angle = Math.atan2(to[0] - from[0], to[1] - from[1]) * (180 / Math.PI); // -180 to 180°
158
+
159
+ // 6 directions version
160
+ if (Math.abs(angle) < 30) { return "N"; }
161
+ else if (angle >= 30 && angle < 90) { return "ENE"; }
162
+ else if (angle >= 90 && angle < 150) { return "ESE"; }
163
+ else if (Math.abs(angle) >= 150) { return "S"; }
164
+ else if (angle <= -30 && angle > -90) { return "WNW"; }
165
+ else if (angle <= -90 && angle > -150) { return "WSW"; }
166
+ }
167
+
168
+ /**
169
+ * Converts result from getPosition or position-updated event into x/y/z coordinates
170
+ *
171
+ * @param {object} pos pitch/yaw as given by PSV
172
+ * @param {number} zoom zoom as given by PSV
173
+ * @returns {object} Coordinates as x/y in degrees and zoom as given by PSV
174
+ * @private
175
+ */
176
+ export function positionToXYZ(pos, zoom = undefined) {
177
+ const res = {
178
+ x: pos.yaw * (180/Math.PI),
179
+ y: pos.pitch * (180/Math.PI)
180
+ };
181
+
182
+ if(zoom !== undefined) { res.z = zoom; }
183
+ return res;
184
+ }
185
+
186
+ /**
187
+ * Converts x/y/z coordinates into PSV position (lat/lon/zoom)
188
+ *
189
+ * @param {number} x The X coordinate (in degrees)
190
+ * @param {number} y The Y coordinate (in degrees)
191
+ * @param {number} z The zoom level (0-100)
192
+ * @returns {object} Position coordinates as yaw/pitch/zoom
193
+ * @private
194
+ */
195
+ export function xyzToPosition(x, y, z) {
196
+ return {
197
+ yaw: x / (180/Math.PI),
198
+ pitch: y / (180/Math.PI),
199
+ zoom: z
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Get the query string for JOSM to load current picture area
205
+ * @returns {string} The query string, or null if not available
206
+ * @private
207
+ */
208
+ export function josmBboxParameters(meta) {
209
+ if(meta) {
210
+ const coords = meta.gps;
211
+ const heading = meta?.properties?.["view:azimuth"];
212
+ const delta = 0.0002;
213
+ const values = {
214
+ left: coords[0] - (heading === null || heading >= 180 ? delta : 0),
215
+ right: coords[0] + (heading === null || heading <= 180 ? delta : 0),
216
+ top: coords[1] + (heading === null || heading <= 90 || heading >= 270 ? delta : 0),
217
+ bottom: coords[1] - (heading === null || (heading >= 90 && heading <= 270) ? delta : 0),
218
+ changeset_source: "Panoramax"
219
+ };
220
+ return Object.entries(values).map(e => e.join("=")).join("&");
221
+ }
222
+ else { return null; }
223
+ }
224
+
225
+ /**
226
+ * Check if code runs in an iframe or in a classic page.
227
+ * @returns {boolean} True if running in iframe
228
+ * @private
229
+ */
230
+ export function isInIframe() {
231
+ try {
232
+ return window.self !== window.top;
233
+ } catch(e) {
234
+ return true;
235
+ }
236
+ }
237
+
238
+
239
+ const INTERNET_FAST_THRESHOLD = 10; // MBit/s
240
+ const INTERNET_FAST_STORAGE = "pnx-internet-fast";
241
+ const INTERNET_FAST_TESTFILE = "https://panoramax.openstreetmap.fr/images/05/ca/2c/98/0111-4baf-b6f3-587bb8847d2e.jpg";
242
+
243
+ /**
244
+ * Check if Internet connection is high-speed or not.
245
+ * @returns {Promise} Resolves on true if high-speed.
246
+ * @private
247
+ */
248
+ export function isInternetFast() {
249
+ // Check if downlink property is available
250
+ try {
251
+ const speed = navigator.connection.downlink; // MBit/s
252
+ return Promise.resolve(speed >= INTERNET_FAST_THRESHOLD);
253
+ }
254
+ // Fallback for other browsers
255
+ catch(e) {
256
+ try {
257
+ // Check if test has been done before and stored
258
+ const isFast = sessionStorage.getItem(INTERNET_FAST_STORAGE);
259
+ if(["true", "false"].includes(isFast)) {
260
+ return Promise.resolve(isFast === "true");
261
+ }
262
+
263
+ // Run download testing
264
+ const startTime = (new Date()).getTime();
265
+ return fetch(INTERNET_FAST_TESTFILE+"?nocache="+startTime)
266
+ .then(async res => [res, await res.blob()])
267
+ .then(([res, blob]) => {
268
+ const size = parseInt(res.headers.get("Content-Length") || blob.size); // Bytes
269
+ const endTime = (new Date()).getTime();
270
+ const duration = (endTime - startTime) / 1000; // Transfer time in seconds
271
+ const speed = (size * 8 / 1024 / 1024) / duration; // MBits/s
272
+ const isFast = speed >= INTERNET_FAST_THRESHOLD;
273
+ sessionStorage.setItem(INTERNET_FAST_STORAGE, isFast ? "true" : "false");
274
+ return isFast;
275
+ })
276
+ .catch(e => {
277
+ console.warn("Failed to run speedtest", e);
278
+ return false;
279
+ });
280
+ }
281
+ // Fallback for browser blocking third-party downloads or sessionStorage
282
+ catch(e) {
283
+ return Promise.resolve(false);
284
+ }
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Get a cookie value
290
+ * @param {str} name The cookie name
291
+ * @returns {str} The cookie value, or null if not found
292
+ * @private
293
+ */
294
+ export function getCookie(name) {
295
+ const parts = document.cookie
296
+ ?.split(";")
297
+ ?.find((row) => row.trimStart().startsWith(`${name}=`))
298
+ ?.split("=");
299
+ if(!parts) { return undefined; }
300
+ parts.shift();
301
+ return parts.join("=");
302
+ }
303
+
304
+ /**
305
+ * Checks if an user account exists
306
+ * @returns {object} Object like {"id", "name"} or null if no authenticated account
307
+ * @private
308
+ */
309
+ export function getUserAccount() {
310
+ const session = getCookie("session");
311
+ const user_id = getCookie("user_id");
312
+ const user_name = getCookie("user_name");
313
+
314
+ return (session && user_id && user_name) ? { id: user_id, name: user_name } : null;
315
+ }
@@ -0,0 +1,93 @@
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
+ * Table cell with a copy link
18
+ * @private
19
+ */
20
+ export function createLinkCell(id, url, title, _t) {
21
+ const link = document.createElement("a");
22
+ link.href = url;
23
+ link.target = "_blank";
24
+ link.title = title;
25
+ link.textContent = id;
26
+
27
+ const buttonContainer = createWebComp("pnx-copy-button", {text: id, _t});
28
+ return [link, buttonContainer];
29
+ }
30
+
31
+ /**
32
+ * Create a light table
33
+ * @private
34
+ */
35
+ export function createTable(className, rows) {
36
+ const table = document.createElement("table");
37
+ table.className = className;
38
+
39
+ rows.forEach(({ section, value, values, classes }) => {
40
+ const tr = document.createElement("tr");
41
+ const th = document.createElement("th");
42
+ th.scope = "row";
43
+ th.textContent = section;
44
+ tr.appendChild(th);
45
+
46
+ const td = document.createElement("td");
47
+ if(classes) { td.classList.add(...classes); }
48
+ if(values) { values.forEach(v => td.appendChild(v)); }
49
+ else if(value instanceof HTMLElement) { td.appendChild(value); }
50
+ else { td.innerHTML = value; }
51
+ tr.appendChild(td);
52
+
53
+ table.appendChild(tr);
54
+ });
55
+
56
+ return table;
57
+ }
58
+
59
+ /**
60
+ * Create a web component with its initial properties
61
+ * @private
62
+ */
63
+ export function createWebComp(tag, props = {}) {
64
+ const wc = document.createElement(tag);
65
+ Object.entries(props).forEach(([k,v]) => {
66
+ if(k.startsWith("_")) { wc[k] = v; }
67
+ else if(k.startsWith("fn")) { wc[k.substring(2)] = v; }
68
+ else if(k.startsWith("on")) { wc.addEventListener(k.substring(2), v); }
69
+ else if(v) { wc.setAttribute(k, v); }
70
+ });
71
+ return wc;
72
+ }
73
+
74
+ /**
75
+ * Listen to parent events that may lead to a menu closure
76
+ * @private
77
+ */
78
+ export function listenForMenuClosure(me, callback) {
79
+ // Other menu opened
80
+ me._parent?.addEventListener("menu-opened", e => {
81
+ if(e.detail.menu != me) { callback(); }
82
+ });
83
+
84
+ // Map click
85
+ me._parent?.onceMapReady?.().then(() => {
86
+ me._parent.map?.on?.("click", () => callback());
87
+ });
88
+
89
+ // Photo click
90
+ me._parent?.oncePSVReady?.().then(() => {
91
+ me._parent.psv.addEventListener("click", () => callback());
92
+ });
93
+ }
@@ -0,0 +1,52 @@
1
+ import CopyButton from "../../../src/components/ui/CopyButton";
2
+
3
+ window.navigator.clipboard = { writeText: jest.fn() };
4
+
5
+ describe("constructor", () => {
6
+ it("listens to click", () => {
7
+ const cb = new CopyButton();
8
+ expect(cb.addEventListener.mock.calls).toMatchSnapshot();
9
+ });
10
+ });
11
+
12
+ describe("_onClick", () => {
13
+ it("works with text", () => {
14
+ const cb = new CopyButton();
15
+ cb.text = "I am a copy text";
16
+ cb._onClick();
17
+ expect(cb._active).toBe(true);
18
+ expect(global.navigator.clipboard.writeText.mock.calls).toMatchSnapshot();
19
+ return new Promise(resolve => setTimeout(() => {
20
+ expect(cb._active).toBe(false);
21
+ resolve();
22
+ }, 2000));
23
+ });
24
+
25
+ it("works with input", () => {
26
+ const i = document.createElement("input");
27
+ i.value = "copy me from input";
28
+ i.id = "input-to-copy";
29
+ document.body.appendChild(i);
30
+
31
+ const cb = new CopyButton();
32
+ cb.input = "input-to-copy";
33
+ cb._onClick();
34
+
35
+ expect(cb._active).toBe(true);
36
+ expect(global.navigator.clipboard.writeText.mock.calls).toMatchSnapshot();
37
+ });
38
+
39
+ it("works with textarea", () => {
40
+ const i = document.createElement("textarea");
41
+ i.appendChild(document.createTextNode("copy me from textarea"));
42
+ i.id = "textarea-to-copy";
43
+ document.body.appendChild(i);
44
+
45
+ const cb = new CopyButton();
46
+ cb.input = "textarea-to-copy";
47
+ cb._onClick();
48
+
49
+ expect(cb._active).toBe(true);
50
+ expect(global.navigator.clipboard.writeText.mock.calls).toMatchSnapshot();
51
+ });
52
+ });
@@ -0,0 +1,54 @@
1
+ import Loader from "../../../src/components/ui/Loader";
2
+
3
+ describe("constructor", () => {
4
+ it("works", () => {
5
+ const l = new Loader();
6
+ expect(l.isVisible()).toBe(true);
7
+ });
8
+ });
9
+
10
+ describe("dismiss", () => {
11
+ it("works on success", () => {
12
+ const p = { dispatchEvent: jest.fn() };
13
+ const l = new Loader();
14
+ l._parent = p;
15
+ l.dismiss();
16
+
17
+ expect(l.isVisible()).toBe(false);
18
+ expect(p.dispatchEvent.mock.calls).toMatchSnapshot();
19
+ });
20
+
21
+ it("works on success + next fct", () => {
22
+ const p = { dispatchEvent: jest.fn() };
23
+ const n = jest.fn();
24
+ const l = new Loader();
25
+ l._parent = p;
26
+ l.dismiss(null, null, n);
27
+
28
+ expect(l.isVisible()).toBe(false);
29
+ expect(p.dispatchEvent.mock.calls).toMatchSnapshot();
30
+ expect(n.mock.calls).toMatchSnapshot();
31
+ });
32
+
33
+ it("works with error", () => {
34
+ const p = { dispatchEvent: jest.fn(), _t: { pnx: {} } };
35
+ const l = new Loader();
36
+ l._parent = p;
37
+ expect(() => l.dismiss(true, "an error")).toThrow(new Error("an error"))
38
+
39
+ expect(l.isVisible()).toBe(true);
40
+ expect(p.dispatchEvent.mock.calls).toMatchSnapshot();
41
+ });
42
+
43
+ it("works with error + next fct", () => {
44
+ const p = { dispatchEvent: jest.fn(), _t: { pnx: {} } };
45
+ const n = jest.fn();
46
+ const l = new Loader();
47
+ l._parent = p;
48
+ expect(() => l.dismiss(true, "an error", n)).toThrow(new Error("an error"))
49
+
50
+ expect(l.isVisible()).toBe(true);
51
+ expect(p.dispatchEvent.mock.calls).toMatchSnapshot();
52
+ expect(l.addEventListener.mock.calls).toMatchSnapshot();
53
+ });
54
+ });
@@ -1,54 +1,13 @@
1
- import Map from "../../src/components/Map";
2
-
3
- jest.mock("maplibre-gl", () => ({
4
- addProtocol: jest.fn(),
5
- AttributionControl: jest.fn(),
6
- GeolocateControl: class {
7
- onAdd() {;}
8
- },
9
- Marker: jest.fn(),
10
- Popup: class {
11
- on() {;}
12
- },
13
- Map: class {
14
- constructor(opts) {
15
- this._mapOpts = opts;
16
- }
17
- getContainer() {
18
- return this._mapOpts.container;
19
- }
20
- addControl() {;}
21
- addSource() {;}
22
- addLayer() {;}
23
- getLayer() {;}
24
- setLayoutProperty() {;}
25
- getStyle() {
26
- return {
27
- layers: [],
28
- sources: {},
29
- metadata: {},
30
- };
31
- }
32
- resize() {;}
33
- on(type, handler) {
34
- if(!this._handlers) { this._handlers = {}; }
35
- if(!this._handlers[type]) { this._handlers[type] = []; }
36
- this._handlers[type].push(handler);
37
- }
38
- _fire(type) {
39
- this._handlers[type].forEach(f => f());
40
- }
41
- },
42
- }));
1
+ import Map from "../../../src/components/ui/Map";
43
2
 
44
3
  const createParent = () => ({
45
4
  addEventListener: jest.fn(),
46
5
  dispatchEvent: jest.fn(),
47
6
  isWidthSmall: jest.fn(),
48
- _options: {
49
- users: ["geovisio"],
50
- },
51
- _api: {
7
+ onceReady: () => Promise.resolve(),
8
+ onceAPIReady: () => Promise.resolve(),
9
+ users: ["geovisio"],
10
+ api: {
52
11
  onceReady: () => Promise.resolve(),
53
12
  getDataBbox: jest.fn(),
54
13
  getPicturesTilesUrl: jest.fn(),
@@ -79,7 +38,7 @@ describe("hasTwoBackgrounds", () => {
79
38
  const p = createParent();
80
39
  const c = document.createElement("div");
81
40
  const m = new Map(p, c);
82
- m.getLayer = (id) => id == "gvs-aerial";
41
+ m.getLayer = (id) => id == "pnx-aerial";
83
42
  expect(m.hasTwoBackgrounds()).toBeTruthy();
84
43
  });
85
44
 
@@ -87,7 +46,7 @@ describe("hasTwoBackgrounds", () => {
87
46
  const p = createParent();
88
47
  const c = document.createElement("div");
89
48
  const m = new Map(p, c);
90
- m.getLayer = (id) => id == "gvs-aerial" ? undefined : {};
49
+ m.getLayer = (id) => id == "pnx-aerial" ? undefined : {};
91
50
  expect(m.hasTwoBackgrounds()).toBeFalsy();
92
51
  });
93
52
  });
@@ -115,14 +74,18 @@ describe("getBackground", () => {
115
74
  describe("setBackground", () => {
116
75
  it("works if raster is enabled", () => {
117
76
  const p = createParent();
118
- p.dispatchEvent = jest.fn();
119
77
  const c = document.createElement("div");
120
78
  const m = new Map(p, c, { raster: { type: "raster" } });
121
79
  m.setLayoutProperty = jest.fn();
122
80
  m.getLayer = () => true;
123
- m.setBackground("aerial");
124
- expect(m.setLayoutProperty.mock.calls).toMatchSnapshot();
125
- expect(p.dispatchEvent.mock.calls).toMatchSnapshot();
81
+ return new Promise(resolve => {
82
+ m.on("background-changed", e => {
83
+ expect(m.setLayoutProperty.mock.calls).toMatchSnapshot();
84
+ expect(e.background).toEqual("aerial");
85
+ resolve();
86
+ });
87
+ m.setBackground("aerial");
88
+ })
126
89
  });
127
90
 
128
91
  it("skips if setting streets and no raster available", () => {
@@ -169,7 +132,7 @@ describe("setVisibleUsers", () => {
169
132
 
170
133
  it("works when user already exist but is hidden", async () => {
171
134
  const p = createParent();
172
- p._options.users = ["blabla", "geovisio"];
135
+ p.users = ["blabla", "geovisio"];
173
136
  const c = document.createElement("div");
174
137
  const m = new Map(p, c);
175
138
  m.setPaintProperty = jest.fn();
@@ -178,12 +141,7 @@ describe("setVisibleUsers", () => {
178
141
  let cptlCount = 0;
179
142
  let deCalls = [];
180
143
  return new Promise(async (resolve) => {
181
- p.dispatchEvent = (e) => {
182
- deCalls.push(e);
183
- if(e.type == "map:users-changed") {
184
- resolve();
185
- }
186
- };
144
+ m.on("users-changed", resolve);
187
145
  await m._postLoad();
188
146
  }).then(() => {
189
147
  m.setLayoutProperty = jest.fn();
@@ -201,7 +159,7 @@ describe("setVisibleUsers", () => {
201
159
  describe("filterUserLayersContent", () => {
202
160
  it("works", async () => {
203
161
  const p = createParent();
204
- p._options.users = ["blabla", "geovisio"];
162
+ p.users = ["blabla", "geovisio"];
205
163
  const c = document.createElement("div");
206
164
  const m = new Map(p, c);
207
165
  m.getSource = () => true;
@@ -217,7 +175,7 @@ describe("filterUserLayersContent", () => {
217
175
  describe("reloadLayersStyles", () => {
218
176
  it("works", async () => {
219
177
  const p = createParent();
220
- p._options.users = ["blabla", "geovisio"];
178
+ p.users = ["blabla", "geovisio"];
221
179
  const c = document.createElement("div");
222
180
  const m = new Map(p, c);
223
181
  m.getSource = () => true;