@panoramax/web-viewer 3.2.3-develop-f219e404 → 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.
- package/.gitlab-ci.yml +3 -0
- package/CHANGELOG.md +19 -0
- package/CODE_OF_CONDUCT.md +1 -1
- package/README.md +1 -1
- package/build/editor.html +10 -1
- package/build/index.css +2 -2
- package/build/index.css.map +1 -1
- package/build/index.html +1 -1
- package/build/index.js +1682 -5
- package/build/index.js.map +1 -1
- package/build/map.html +1 -1
- package/build/viewer.html +10 -1
- package/build/widgets.html +1 -0
- package/config/jest/mocks.js +172 -0
- package/config/paths.js +1 -0
- package/config/webpack.config.js +26 -0
- package/docs/03_URL_settings.md +3 -11
- package/docs/05_Compatibility.md +59 -76
- package/docs/09_Develop.md +30 -11
- package/docs/90_Releases.md +2 -2
- package/docs/images/class_diagram.drawio +28 -28
- package/docs/images/class_diagram.jpg +0 -0
- package/docs/index.md +112 -0
- package/docs/reference/components/core/Basic.md +153 -0
- package/docs/reference/components/core/CoverageMap.md +160 -0
- package/docs/reference/components/core/Editor.md +172 -0
- package/docs/reference/components/core/Viewer.md +288 -0
- package/docs/reference/components/layout/CorneredGrid.md +29 -0
- package/docs/reference/components/layout/Mini.md +45 -0
- package/docs/reference/components/menus/MapBackground.md +32 -0
- package/docs/reference/components/menus/MapFilters.md +15 -0
- package/docs/reference/components/menus/MapLayers.md +15 -0
- package/docs/reference/components/menus/MapLegend.md +15 -0
- package/docs/reference/components/menus/PictureLegend.md +15 -0
- package/docs/reference/components/menus/PictureMetadata.md +15 -0
- package/docs/reference/components/menus/PlayerOptions.md +15 -0
- package/docs/reference/components/menus/QualityScoreDoc.md +15 -0
- package/docs/reference/components/menus/ReportForm.md +15 -0
- package/docs/reference/components/menus/ShareMenu.md +15 -0
- package/docs/reference/components/ui/Button.md +39 -0
- package/docs/reference/components/ui/ButtonGroup.md +36 -0
- package/docs/reference/components/ui/CopyButton.md +35 -0
- package/docs/reference/components/ui/Grade.md +32 -0
- package/docs/reference/components/ui/LinkButton.md +44 -0
- package/docs/reference/components/ui/Loader.md +54 -0
- package/docs/reference/components/ui/Map.md +214 -0
- package/docs/reference/components/ui/MapMore.md +233 -0
- package/docs/reference/components/ui/Photo.md +369 -0
- package/docs/reference/components/ui/Popup.md +56 -0
- package/docs/reference/components/ui/QualityScore.md +45 -0
- package/docs/reference/components/ui/SearchBar.md +63 -0
- package/docs/reference/components/ui/TogglableGroup.md +39 -0
- package/docs/reference/components/ui/widgets/GeoSearch.md +32 -0
- package/docs/reference/components/ui/widgets/Legend.md +32 -0
- package/docs/reference/components/ui/widgets/MapFiltersButton.md +33 -0
- package/docs/reference/components/ui/widgets/MapLayersButton.md +15 -0
- package/docs/reference/components/ui/widgets/Player.md +32 -0
- package/docs/reference/components/ui/widgets/Share.md +15 -0
- package/docs/reference/components/ui/widgets/Zoom.md +15 -0
- package/docs/reference/utils/API.md +311 -0
- package/docs/reference/utils/InitParameters.md +67 -0
- package/docs/reference/utils/URLHandler.md +102 -0
- package/docs/reference.md +73 -0
- package/docs/shortcuts.md +11 -0
- package/docs/tutorials/aerial_imagery.md +19 -0
- package/docs/tutorials/authentication.md +10 -0
- package/docs/tutorials/custom_widgets.md +64 -0
- package/docs/tutorials/map_style.md +27 -0
- package/docs/tutorials/migrate_v4.md +122 -0
- package/docs/tutorials/synced_coverage.md +42 -0
- package/mkdocs.yml +60 -5
- package/package.json +10 -7
- package/public/editor.html +21 -29
- package/public/index.html +3 -3
- package/public/map.html +19 -18
- package/public/viewer.html +18 -24
- package/public/widgets.html +265 -0
- package/scripts/doc.js +77 -0
- package/src/components/core/Basic.css +44 -0
- package/src/components/core/Basic.js +258 -0
- package/src/components/core/CoverageMap.css +9 -0
- package/src/components/core/CoverageMap.js +105 -0
- package/src/components/core/Editor.css +23 -0
- package/src/components/core/Editor.js +354 -0
- package/src/components/core/Viewer.css +109 -0
- package/src/components/core/Viewer.js +707 -0
- package/src/components/core/index.js +11 -0
- package/src/components/index.js +13 -0
- package/src/components/layout/CorneredGrid.js +109 -0
- package/src/components/layout/Mini.js +117 -0
- package/src/components/layout/index.js +7 -0
- package/src/components/menus/MapBackground.js +106 -0
- package/src/components/menus/MapFilters.js +386 -0
- package/src/components/menus/MapLayers.js +143 -0
- package/src/components/menus/MapLegend.js +54 -0
- package/src/components/menus/PictureLegend.js +103 -0
- package/src/components/menus/PictureMetadata.js +188 -0
- package/src/components/menus/PlayerOptions.js +96 -0
- package/src/components/menus/QualityScoreDoc.js +36 -0
- package/src/components/menus/ReportForm.js +133 -0
- package/src/components/menus/Share.js +228 -0
- package/src/components/menus/index.js +15 -0
- package/src/components/styles.js +365 -0
- package/src/components/ui/Button.js +75 -0
- package/src/components/ui/ButtonGroup.css +49 -0
- package/src/components/ui/ButtonGroup.js +68 -0
- package/src/components/ui/CopyButton.js +71 -0
- package/src/components/ui/Grade.js +54 -0
- package/src/components/ui/LinkButton.js +68 -0
- package/src/components/ui/Loader.js +188 -0
- package/src/components/{Map.css → ui/Map.css} +5 -17
- package/src/components/{Map.js → ui/Map.js} +114 -138
- package/src/components/ui/MapMore.js +324 -0
- package/src/components/{Photo.css → ui/Photo.css} +6 -6
- package/src/components/{Photo.js → ui/Photo.js} +279 -90
- package/src/components/ui/Popup.js +145 -0
- package/src/components/ui/QualityScore.js +152 -0
- package/src/components/ui/SearchBar.js +363 -0
- package/src/components/ui/TogglableGroup.js +162 -0
- package/src/components/ui/index.js +20 -0
- package/src/components/ui/widgets/GeoSearch.css +21 -0
- package/src/components/ui/widgets/GeoSearch.js +139 -0
- package/src/components/ui/widgets/Legend.js +51 -0
- package/src/components/ui/widgets/MapFiltersButton.js +104 -0
- package/src/components/ui/widgets/MapLayersButton.js +79 -0
- package/src/components/ui/widgets/Player.css +7 -0
- package/src/components/ui/widgets/Player.js +148 -0
- package/src/components/ui/widgets/Share.js +30 -0
- package/src/components/ui/widgets/Zoom.js +82 -0
- package/src/components/ui/widgets/index.js +12 -0
- package/src/img/panoramax.svg +13 -0
- package/src/img/switch_big.svg +20 -10
- package/src/index.js +6 -9
- package/src/translations/da.json +1 -1
- package/src/translations/de.json +1 -1
- package/src/translations/en.json +5 -3
- package/src/translations/eo.json +1 -1
- package/src/translations/es.json +1 -1
- package/src/translations/fr.json +5 -3
- package/src/translations/hu.json +1 -1
- package/src/translations/it.json +1 -1
- package/src/translations/ja.json +1 -1
- package/src/translations/nl.json +1 -1
- package/src/translations/pl.json +1 -1
- package/src/translations/sv.json +33 -3
- package/src/translations/zh_Hant.json +1 -1
- package/src/utils/API.js +74 -42
- package/src/utils/InitParameters.js +354 -0
- package/src/utils/URLHandler.js +364 -0
- package/src/utils/geocoder.js +116 -0
- package/src/utils/{I18n.js → i18n.js} +3 -1
- package/src/utils/index.js +11 -0
- package/src/utils/{Map.js → map.js} +216 -80
- package/src/utils/picture.js +433 -0
- package/src/utils/utils.js +315 -0
- package/src/utils/widgets.js +93 -0
- package/tests/components/ui/CopyButton.test.js +52 -0
- package/tests/components/ui/Loader.test.js +54 -0
- package/tests/components/{Map.test.js → ui/Map.test.js} +19 -61
- package/tests/components/{Photo.test.js → ui/Photo.test.js} +89 -57
- package/tests/components/ui/Popup.test.js +24 -0
- package/tests/components/ui/QualityScore.test.js +17 -0
- package/tests/components/ui/SearchBar.test.js +107 -0
- package/tests/components/ui/__snapshots__/CopyButton.test.js.snap +34 -0
- package/tests/components/ui/__snapshots__/Loader.test.js.snap +56 -0
- package/tests/components/{__snapshots__ → ui/__snapshots__}/Map.test.js.snap +11 -38
- package/tests/components/{__snapshots__ → ui/__snapshots__}/Photo.test.js.snap +57 -4
- package/tests/components/ui/__snapshots__/Popup.test.js.snap +29 -0
- package/tests/components/ui/__snapshots__/QualityScore.test.js.snap +11 -0
- package/tests/components/ui/__snapshots__/SearchBar.test.js.snap +65 -0
- package/tests/utils/API.test.js +1 -14
- package/tests/utils/InitParameters.test.js +485 -0
- package/tests/utils/URLHandler.test.js +350 -0
- package/tests/utils/__snapshots__/URLHandler.test.js.snap +21 -0
- package/tests/utils/__snapshots__/picture.test.js.snap +315 -0
- package/tests/utils/__snapshots__/widgets.test.js.snap +19 -0
- package/tests/utils/geocoder.test.js +37 -0
- package/tests/utils/{I18n.test.js → i18n.test.js} +1 -1
- package/tests/utils/map.test.js +67 -0
- package/tests/utils/picture.test.js +745 -0
- package/tests/utils/utils.test.js +288 -0
- package/tests/utils/widgets.test.js +90 -0
- package/docs/01_Start.md +0 -149
- package/docs/02_Usage.md +0 -831
- package/docs/04_Advanced_examples.md +0 -216
- package/src/Editor.css +0 -37
- package/src/Editor.js +0 -361
- package/src/StandaloneMap.js +0 -114
- package/src/Viewer.css +0 -203
- package/src/Viewer.js +0 -1246
- package/src/components/CoreView.css +0 -70
- package/src/components/CoreView.js +0 -175
- package/src/components/Loader.css +0 -74
- package/src/components/Loader.js +0 -120
- package/src/utils/Exif.js +0 -193
- package/src/utils/Utils.js +0 -631
- package/src/utils/Widgets.js +0 -562
- package/src/viewer/URLHash.js +0 -469
- package/src/viewer/Widgets.css +0 -880
- package/src/viewer/Widgets.js +0 -1470
- package/tests/Editor.test.js +0 -126
- package/tests/StandaloneMap.test.js +0 -45
- package/tests/Viewer.test.js +0 -366
- package/tests/__snapshots__/Editor.test.js.snap +0 -298
- package/tests/__snapshots__/StandaloneMap.test.js.snap +0 -30
- package/tests/__snapshots__/Viewer.test.js.snap +0 -195
- package/tests/components/CoreView.test.js +0 -92
- package/tests/components/Loader.test.js +0 -38
- package/tests/components/__snapshots__/Loader.test.js.snap +0 -15
- package/tests/utils/Exif.test.js +0 -124
- package/tests/utils/Map.test.js +0 -113
- package/tests/utils/Utils.test.js +0 -300
- package/tests/utils/Widgets.test.js +0 -107
- package/tests/utils/__snapshots__/Exif.test.js.snap +0 -43
- package/tests/utils/__snapshots__/Utils.test.js.snap +0 -41
- package/tests/utils/__snapshots__/Widgets.test.js.snap +0 -44
- package/tests/viewer/URLHash.test.js +0 -559
- package/tests/viewer/Widgets.test.js +0 -127
- package/tests/viewer/__snapshots__/URLHash.test.js.snap +0 -108
- package/tests/viewer/__snapshots__/Widgets.test.js.snap +0 -403
- /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 "
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 == "
|
|
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 == "
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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;
|