@panoramax/web-viewer 3.0.2-develop-a8ea8e60

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 (125) hide show
  1. package/.dockerignore +6 -0
  2. package/.gitlab-ci.yml +71 -0
  3. package/CHANGELOG.md +428 -0
  4. package/CODE_OF_CONDUCT.md +134 -0
  5. package/Dockerfile +14 -0
  6. package/LICENSE +21 -0
  7. package/README.md +39 -0
  8. package/build/editor.html +1 -0
  9. package/build/index.css +36 -0
  10. package/build/index.css.map +1 -0
  11. package/build/index.html +1 -0
  12. package/build/index.js +25 -0
  13. package/build/index.js.map +1 -0
  14. package/build/map.html +1 -0
  15. package/build/viewer.html +1 -0
  16. package/config/env.js +104 -0
  17. package/config/getHttpsConfig.js +66 -0
  18. package/config/getPackageJson.js +25 -0
  19. package/config/jest/babelTransform.js +29 -0
  20. package/config/jest/cssTransform.js +14 -0
  21. package/config/jest/fileTransform.js +40 -0
  22. package/config/modules.js +134 -0
  23. package/config/paths.js +72 -0
  24. package/config/pnpTs.js +35 -0
  25. package/config/webpack/persistentCache/createEnvironmentHash.js +9 -0
  26. package/config/webpack.config.js +885 -0
  27. package/config/webpackDevServer.config.js +127 -0
  28. package/docs/01_Start.md +149 -0
  29. package/docs/02_Usage.md +828 -0
  30. package/docs/03_URL_settings.md +140 -0
  31. package/docs/04_Advanced_examples.md +214 -0
  32. package/docs/05_Compatibility.md +85 -0
  33. package/docs/09_Develop.md +62 -0
  34. package/docs/90_Releases.md +27 -0
  35. package/docs/images/class_diagram.drawio +129 -0
  36. package/docs/images/class_diagram.jpg +0 -0
  37. package/docs/images/screenshot.jpg +0 -0
  38. package/mkdocs.yml +45 -0
  39. package/package.json +254 -0
  40. package/public/editor.html +54 -0
  41. package/public/favicon.ico +0 -0
  42. package/public/index.html +59 -0
  43. package/public/map.html +53 -0
  44. package/public/viewer.html +67 -0
  45. package/scripts/build.js +217 -0
  46. package/scripts/start.js +176 -0
  47. package/scripts/test.js +52 -0
  48. package/src/Editor.css +37 -0
  49. package/src/Editor.js +359 -0
  50. package/src/StandaloneMap.js +114 -0
  51. package/src/Viewer.css +203 -0
  52. package/src/Viewer.js +1186 -0
  53. package/src/components/CoreView.css +64 -0
  54. package/src/components/CoreView.js +159 -0
  55. package/src/components/Loader.css +56 -0
  56. package/src/components/Loader.js +111 -0
  57. package/src/components/Map.css +65 -0
  58. package/src/components/Map.js +841 -0
  59. package/src/components/Photo.css +36 -0
  60. package/src/components/Photo.js +687 -0
  61. package/src/img/arrow_360.svg +14 -0
  62. package/src/img/arrow_flat.svg +11 -0
  63. package/src/img/arrow_triangle.svg +10 -0
  64. package/src/img/arrow_turn.svg +9 -0
  65. package/src/img/bg_aerial.jpg +0 -0
  66. package/src/img/bg_streets.jpg +0 -0
  67. package/src/img/loader_base.jpg +0 -0
  68. package/src/img/loader_hd.jpg +0 -0
  69. package/src/img/logo_dead.svg +91 -0
  70. package/src/img/marker.svg +17 -0
  71. package/src/img/marker_blue.svg +20 -0
  72. package/src/img/switch_big.svg +44 -0
  73. package/src/img/switch_mini.svg +48 -0
  74. package/src/index.js +10 -0
  75. package/src/translations/de.json +163 -0
  76. package/src/translations/en.json +164 -0
  77. package/src/translations/eo.json +6 -0
  78. package/src/translations/es.json +164 -0
  79. package/src/translations/fi.json +1 -0
  80. package/src/translations/fr.json +164 -0
  81. package/src/translations/hu.json +133 -0
  82. package/src/translations/nl.json +1 -0
  83. package/src/translations/zh_Hant.json +136 -0
  84. package/src/utils/API.js +709 -0
  85. package/src/utils/Exif.js +198 -0
  86. package/src/utils/I18n.js +75 -0
  87. package/src/utils/Map.js +382 -0
  88. package/src/utils/PhotoAdapter.js +45 -0
  89. package/src/utils/Utils.js +568 -0
  90. package/src/utils/Widgets.js +477 -0
  91. package/src/viewer/URLHash.js +334 -0
  92. package/src/viewer/Widgets.css +711 -0
  93. package/src/viewer/Widgets.js +1196 -0
  94. package/tests/Editor.test.js +125 -0
  95. package/tests/StandaloneMap.test.js +44 -0
  96. package/tests/Viewer.test.js +363 -0
  97. package/tests/__snapshots__/Editor.test.js.snap +300 -0
  98. package/tests/__snapshots__/StandaloneMap.test.js.snap +30 -0
  99. package/tests/__snapshots__/Viewer.test.js.snap +195 -0
  100. package/tests/components/CoreView.test.js +91 -0
  101. package/tests/components/Loader.test.js +38 -0
  102. package/tests/components/Map.test.js +230 -0
  103. package/tests/components/Photo.test.js +335 -0
  104. package/tests/components/__snapshots__/Loader.test.js.snap +15 -0
  105. package/tests/components/__snapshots__/Map.test.js.snap +767 -0
  106. package/tests/components/__snapshots__/Photo.test.js.snap +205 -0
  107. package/tests/data/Map_geocoder_ban.json +36 -0
  108. package/tests/data/Map_geocoder_nominatim.json +56 -0
  109. package/tests/data/Viewer_pictures_1.json +148 -0
  110. package/tests/setupTests.js +5 -0
  111. package/tests/utils/API.test.js +906 -0
  112. package/tests/utils/Exif.test.js +124 -0
  113. package/tests/utils/I18n.test.js +28 -0
  114. package/tests/utils/Map.test.js +105 -0
  115. package/tests/utils/Utils.test.js +300 -0
  116. package/tests/utils/Widgets.test.js +107 -0
  117. package/tests/utils/__snapshots__/API.test.js.snap +132 -0
  118. package/tests/utils/__snapshots__/Exif.test.js.snap +43 -0
  119. package/tests/utils/__snapshots__/Map.test.js.snap +48 -0
  120. package/tests/utils/__snapshots__/Utils.test.js.snap +41 -0
  121. package/tests/utils/__snapshots__/Widgets.test.js.snap +44 -0
  122. package/tests/viewer/URLHash.test.js +537 -0
  123. package/tests/viewer/Widgets.test.js +127 -0
  124. package/tests/viewer/__snapshots__/URLHash.test.js.snap +98 -0
  125. package/tests/viewer/__snapshots__/Widgets.test.js.snap +393 -0
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Read float value from EXIF tags (to handle fractions & all)
3
+ * @param {*} val The input EXIF tag value
4
+ * @returns {number|undefined} The parsed value, or undefined if value is not readable
5
+ * @private
6
+ */
7
+ export function getExifFloat(val) {
8
+ // Null-like values
9
+ if(
10
+ [null, undefined, ""].includes(val)
11
+ || typeof val === "string" && val.trim() === ""
12
+ ) {
13
+ return undefined;
14
+ }
15
+ // Already valid number
16
+ else if(typeof val === "number") {
17
+ return val;
18
+ }
19
+ // String
20
+ else if(typeof val === "string") {
21
+ // Check if looks like a fraction
22
+ if(/^-?\d+(\.\d+)?\/-?\d+(\.\d+)?$/.test(val)) {
23
+ const parts = val.split("/").map(p => parseFloat(p));
24
+ return parts[0] / parts[1];
25
+ }
26
+
27
+ // Try a direct cast to float
28
+ try { return parseFloat(val); }
29
+ catch(e) {} // eslint-disable-line no-empty
30
+
31
+ // Unrecognized
32
+ return undefined;
33
+ }
34
+ else { return undefined; }
35
+ }
36
+
37
+ /**
38
+ * Find in picture metadata the GPS precision.
39
+ * @param {object} picture The GeoJSON picture feature
40
+ * @returns {string} The precision value (poor, fair, moderate, good, excellent, ideal, unknown)
41
+ * @private
42
+ */
43
+ export function getGPSPrecision(picture) {
44
+ let quality = "unknown";
45
+ const gpsHPosError = getExifFloat(picture?.properties?.exif?.["Exif.GPSInfo.GPSHPositioningError"]);
46
+ const gpsDop = getExifFloat(picture?.properties?.exif?.["Exif.GPSInfo.GPSDOP"]);
47
+
48
+ if(gpsHPosError !== undefined) {
49
+ if(gpsHPosError < 0.5) { quality = "ideal"; }
50
+ else if(gpsHPosError < 1) { quality = "excellent"; }
51
+ else if(gpsHPosError < 3) { quality = "good"; }
52
+ else if(gpsHPosError < 7) { quality = "moderate"; }
53
+ else if(gpsHPosError < 10) { quality = "fair"; }
54
+ else { quality = "poor"; }
55
+ }
56
+ else if(gpsDop !== undefined) {
57
+ if(gpsDop < 1) { quality = "ideal"; }
58
+ else if(gpsDop < 2) { quality = "excellent"; }
59
+ else if(gpsDop < 5) { quality = "good"; }
60
+ else if(gpsDop < 10) { quality = "moderate"; }
61
+ else if(gpsDop < 20) { quality = "fair"; }
62
+ else { quality = "poor"; }
63
+ }
64
+
65
+ return quality;
66
+ }
67
+
68
+ /**
69
+ * Compute PSV sphere correction based on picture metadata & EXIF tags.
70
+ * @param {object} picture The GeoJSON picture feature
71
+ * @returns {object} The PSV sphereCorrection value
72
+ * @private
73
+ */
74
+ export function getSphereCorrection(picture) {
75
+ // Photo direction
76
+ let dir = picture.properties?.["view:azimuth"];
77
+ if(dir === undefined) {
78
+ const v = getExifFloat(picture.properties?.exif?.["Exif.GPSInfo.GPSImgDirection"]);
79
+ if(v !== undefined) {
80
+ dir = v;
81
+ }
82
+ }
83
+ dir = dir || 0;
84
+
85
+ // Yaw
86
+ let yaw = picture.properties?.["pers:yaw"];
87
+ let exifFallbacks = ["Xmp.GPano.PoseHeadingDegrees", "Xmp.Camera.Yaw", "Exif.MpfInfo.MPFYawAngle"];
88
+ if(yaw === undefined) {
89
+ for(let exif of exifFallbacks) {
90
+ const v = getExifFloat(picture.properties?.exif?.[exif]);
91
+ if(v !== undefined) {
92
+ yaw = v;
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ yaw = yaw || 0;
98
+
99
+ // Check if yaw is applicable: different from photo direction
100
+ if(Math.round(dir) === Math.round(yaw) && yaw > 0) {
101
+ console.warn("Picture with UUID", picture.id, "has same GPS Image direction and Yaw, could cause rendering issues");
102
+ // yaw = 0;
103
+ }
104
+
105
+ // Pitch
106
+ let pitch = picture.properties?.["pers:pitch"];
107
+ exifFallbacks = ["Xmp.GPano.PosePitchDegrees", "Xmp.Camera.Pitch", "Exif.MpfInfo.MPFPitchAngle"];
108
+ if(pitch === undefined) {
109
+ for(let exif of exifFallbacks) {
110
+ const v = getExifFloat(picture.properties?.exif?.[exif]);
111
+ if(v !== undefined) {
112
+ pitch = v;
113
+ break;
114
+ }
115
+ }
116
+ }
117
+ pitch = pitch || 0;
118
+
119
+ // Roll
120
+ let roll = picture.properties?.["pers:roll"];
121
+ exifFallbacks = ["Xmp.GPano.PoseRollDegrees", "Xmp.Camera.Roll", "Exif.MpfInfo.MPFRollAngle"];
122
+ if(roll === undefined) {
123
+ for(let exif of exifFallbacks) {
124
+ const v = getExifFloat(picture.properties?.exif?.[exif]);
125
+ if(v !== undefined) {
126
+ roll = v;
127
+ break;
128
+ }
129
+ }
130
+ }
131
+ roll = roll || 0;
132
+
133
+ // Send result
134
+ return {
135
+ pan: yaw * Math.PI / 180,
136
+ tilt: pitch * Math.PI / 180,
137
+ roll: roll * Math.PI / 180,
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Compute PSV panoData for cropped panorama based on picture metadata & EXIF tags.
143
+ * @param {object} picture The GeoJSON picture feature
144
+ * @returns {object} The PSV panoData values
145
+ * @private
146
+ */
147
+ export function getCroppedPanoData(picture) {
148
+ let res;
149
+
150
+ if(picture.properties?.["pers:interior_orientation"]) {
151
+ if(
152
+ picture.properties["pers:interior_orientation"]?.["visible_area"]
153
+ && picture.properties["pers:interior_orientation"]?.["sensor_array_dimensions"]
154
+ ) {
155
+ const va = picture.properties["pers:interior_orientation"]["visible_area"];
156
+ const sad = picture.properties["pers:interior_orientation"]["sensor_array_dimensions"];
157
+ try {
158
+ res = {
159
+ fullWidth: parseInt(sad[0]),
160
+ fullHeight: parseInt(sad[1]),
161
+ croppedX: parseInt(va[0]),
162
+ croppedY: parseInt(va[1]),
163
+ croppedWidth: parseInt(sad[0]) - parseInt(va[2]) - parseInt(va[0]),
164
+ croppedHeight: parseInt(sad[1]) - parseInt(va[3]) - parseInt(va[1]),
165
+ };
166
+ }
167
+ catch(e) {
168
+ console.warn("Invalid pers:interior_orientation values for cropped panorama "+picture.id);
169
+ }
170
+ }
171
+ }
172
+
173
+ if(!res && picture.properties?.exif) {
174
+ try {
175
+ res = {
176
+ fullWidth: parseInt(picture.properties.exif?.["Xmp.GPano.FullPanoWidthPixels"]),
177
+ fullHeight: parseInt(picture.properties.exif?.["Xmp.GPano.FullPanoHeightPixels"]),
178
+ croppedX: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaLeftPixels"]),
179
+ croppedY: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaTopPixels"]),
180
+ croppedWidth: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaImageWidthPixels"]),
181
+ croppedHeight: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaImageHeightPixels"]),
182
+ };
183
+ }
184
+ catch(e) {
185
+ console.warn("Invalid XMP.GPano values for cropped panorama "+picture.id);
186
+ }
187
+ }
188
+
189
+ // Check if crop is really necessary
190
+ if(res) {
191
+ res = Object.fromEntries(Object.entries(res || {}).filter(e => !isNaN(e[1])));
192
+ if(res.fullWidth == res.croppedWidth && res.fullHeight == res.croppedHeight) {
193
+ res = {};
194
+ }
195
+ }
196
+
197
+ return res || {};
198
+ }
@@ -0,0 +1,75 @@
1
+ import T_de from "../translations/de.json";
2
+ import T_en from "../translations/en.json";
3
+ import T_es from "../translations/es.json";
4
+ import T_fr from "../translations/fr.json";
5
+ import T_hu from "../translations/hu.json";
6
+ import T_zh_Hant from "../translations/zh_Hant.json";
7
+
8
+ const FALLBACK_LOCALE = "en";
9
+ const TRANSLATIONS = {
10
+ "de": T_de, "en": T_en, "es": T_es, "fr": T_fr, "hu": T_hu, "zh_Hant": T_zh_Hant,
11
+ };
12
+
13
+ const autoDetectLocale = () => {
14
+ for (const navigatorLang of window.navigator.languages) {
15
+ let language = navigatorLang;
16
+ // Convert browser code to weblate code
17
+ switch (language) {
18
+ case "zh-TW":
19
+ case "zh-HK":
20
+ case "zh-MO":
21
+ language = "zh_Hant";
22
+ break;
23
+ case "zh-CN":
24
+ case "zh-SG":
25
+ language = "zh_Hans";
26
+ break;
27
+ default:
28
+ if (language.length > 2) {
29
+ language = navigatorLang.substring(0, 2);
30
+ }
31
+ break;
32
+ }
33
+ const pair = Object.entries(TRANSLATIONS).find((pair) => pair[0] === language);
34
+ if (pair) {
35
+ return pair[0];
36
+ }
37
+ }
38
+ return FALLBACK_LOCALE;
39
+ };
40
+ /**
41
+ * Get text labels translations in given language
42
+ *
43
+ * @param {string} lang The language code (fr, en)
44
+ * @returns {object} Translations in given language, with fallback to english
45
+ * @private
46
+ */
47
+ export function getTranslations(lang = "") {
48
+ const myTr = JSON.parse(JSON.stringify(T_en));
49
+ let preferedTr;
50
+
51
+ // No specific lang set -> use browser lang
52
+ if(!lang) {
53
+ lang = autoDetectLocale();
54
+ }
55
+
56
+ // Lang exists -> send it
57
+ if(TRANSLATIONS[lang] && lang !== "en") {
58
+ preferedTr = TRANSLATIONS[lang];
59
+ }
60
+
61
+ // Look for primary lang
62
+ if(lang.length > 2) {
63
+ const primaryLang = lang.substring(0, 2);
64
+ if(TRANSLATIONS[primaryLang]) { preferedTr = TRANSLATIONS[primaryLang]; }
65
+ }
66
+
67
+ // Merge labels to avoid missing ones
68
+ if(preferedTr) {
69
+ Object.entries(preferedTr).forEach(([k1, v1]) => {
70
+ Object.assign(myTr[k1], v1);
71
+ });
72
+ }
73
+
74
+ return myTr;
75
+ }
@@ -0,0 +1,382 @@
1
+ // DO NOT REMOVE THE "!": bundled builds breaks otherwise !!!
2
+ import maplibregl from "!maplibre-gl";
3
+ import LoaderImg from "../img/marker.svg";
4
+ import { COLORS } from "./Utils";
5
+
6
+ export const DEFAULT_TILES = "https://panoramax.openstreetmap.fr/pmtiles/basic.json";
7
+ export const RASTER_LAYER_ID = "gvs-aerial";
8
+
9
+ export const TILES_PICTURES_ZOOM = 15;
10
+ export const TILES_PICTURES_SYMBOL_ZOOM = 18;
11
+
12
+ export const VECTOR_STYLES = {
13
+ PICTURES: {
14
+ "paint": {
15
+ "circle-radius": ["interpolate", ["linear"], ["zoom"],
16
+ TILES_PICTURES_ZOOM, 4.5,
17
+ TILES_PICTURES_SYMBOL_ZOOM, 6,
18
+ 24, 12
19
+ ],
20
+ "circle-opacity": ["interpolate", ["linear"], ["zoom"],
21
+ TILES_PICTURES_ZOOM, 0,
22
+ TILES_PICTURES_ZOOM+1, 1
23
+ ],
24
+ "circle-stroke-color": "#ffffff",
25
+ "circle-stroke-width": ["interpolate", ["linear"], ["zoom"],
26
+ TILES_PICTURES_ZOOM+1, 0,
27
+ TILES_PICTURES_ZOOM+2, 1,
28
+ TILES_PICTURES_SYMBOL_ZOOM, 1.5,
29
+ 24, 3
30
+ ],
31
+ },
32
+ "layout": {}
33
+ },
34
+ PICTURES_SYMBOLS: {
35
+ "paint": {
36
+ "icon-opacity": ["interpolate", ["linear"], ["zoom"], TILES_PICTURES_SYMBOL_ZOOM, 0, TILES_PICTURES_SYMBOL_ZOOM+1, 1],
37
+ },
38
+ "layout": {
39
+ "icon-image": ["case", ["==", ["get", "type"], "equirectangular"], "gvs-arrow-360", "gvs-arrow-flat"],
40
+ "icon-size": ["interpolate", ["linear"], ["zoom"], TILES_PICTURES_SYMBOL_ZOOM, 0.5, 24, 1],
41
+ "icon-rotate": ["to-number", ["get", "heading"]],
42
+ "icon-allow-overlap": true,
43
+ },
44
+ },
45
+ SEQUENCES: {
46
+ "paint": {
47
+ "line-width": ["interpolate", ["linear"], ["zoom"], 0, 0.5, 10, 2, 14, 4, 16, 5, 22, 3],
48
+ },
49
+ "layout": {
50
+ "line-cap": "square",
51
+ }
52
+ },
53
+ SEQUENCES_PLUS: {
54
+ "paint": {
55
+ "line-width": ["interpolate", ["linear"], ["zoom"], 0, 15, TILES_PICTURES_ZOOM+1, 30, TILES_PICTURES_ZOOM+2, 0],
56
+ "line-opacity": 0,
57
+ "line-color": "#ff0000",
58
+ },
59
+ "layout": {
60
+ "line-cap": "square",
61
+ }
62
+ }
63
+ };
64
+
65
+ /**
66
+ * Get the GIF shown while thumbnail loads
67
+ * @param {object} lang Translations
68
+ * @returns The DOM element for this GIF
69
+ * @private
70
+ */
71
+ export function getThumbGif(lang) {
72
+ const thumbGif = document.createElement("img");
73
+ thumbGif.src = LoaderImg;
74
+ thumbGif.alt = lang.loading;
75
+ thumbGif.title = lang.loading;
76
+ thumbGif.classList.add("gvs-map-thumb", "gvs-map-thumb-loader");
77
+ return thumbGif;
78
+ }
79
+
80
+ /**
81
+ * Is given layer a label layer.
82
+ *
83
+ * This is useful for inserting new vector layer before labels in MapLibre.
84
+ * @param {object} l The layer to check
85
+ * @returns {boolean} True if it's a label layer
86
+ * @private
87
+ */
88
+ export function isLabelLayer(l) {
89
+ return l.type === "symbol"
90
+ && l?.layout?.["text-field"]
91
+ && (l.minzoom === undefined || l.minzoom < 15);
92
+ }
93
+
94
+ /**
95
+ * Create all-in-one map style for MapLibre GL JS
96
+ *
97
+ * @param {CoreView} parent The parent view
98
+ * @param {object} options Options from Map component
99
+ * @param {object} [options.raster] The MapLibre raster source for aerial background. This must be a JSON object following [MapLibre raster source definition](https://maplibre.org/maplibre-style-spec/sources/#raster).
100
+ * @param {string} [options.background] Choose default map background to display (streets or aerial, if raster aerial background available). Defaults to street.
101
+ * @param {object} [options.supplementaryStyle] Additional style properties (completing CoreView style and STAC API style)
102
+ * @returns {object} The full MapLibre style
103
+ * @private
104
+ */
105
+ export function combineStyles(parent, options) {
106
+ // Get basic vector styles
107
+ const style = parent._api.getMapStyle();
108
+
109
+ // Complete styles
110
+ style.layers = style.layers.concat(getMissingLayerStyles(style.sources, style.layers));
111
+
112
+ // Complementary style
113
+ if(options.supplementaryStyle) {
114
+ Object.assign(style.sources, options.supplementaryStyle.sources || {});
115
+ style.layers = style.layers.concat(options.supplementaryStyle.layers || []);
116
+ }
117
+
118
+ // Aerial imagery background
119
+ if(options.raster) {
120
+ style.sources["gvs-aerial"] = options.raster;
121
+ style.layers.push({
122
+ "id": RASTER_LAYER_ID,
123
+ "type": "raster",
124
+ "source": "gvs-aerial",
125
+ "layout": {
126
+ "visibility": options.background === "aerial" ? "visible" : "none",
127
+ }
128
+ });
129
+ }
130
+
131
+ // Filter out general tiles if necessary
132
+ if(!parent._options?.users?.includes("geovisio")) {
133
+ style.layers.forEach(l => {
134
+ if(l.source === "geovisio") {
135
+ if(!l.layout) { l.layout = {}; }
136
+ l.layout.visibility = "none";
137
+ }
138
+ });
139
+ }
140
+
141
+ // Order layers (base, geovisio, labels)
142
+ style.layers.sort((a,b) => {
143
+ if(isLabelLayer(a) && !isLabelLayer(b)) { return 1; }
144
+ else if(!isLabelLayer(a) && isLabelLayer(b)) { return -1; }
145
+ else {
146
+ if(a.id.startsWith("geovisio") && !b.id.startsWith("geovisio")) { return 1; }
147
+ else if(!a.id.startsWith("geovisio") && b.id.startsWith("geovisio")) { return -1; }
148
+ else {
149
+ if(a.id.endsWith("_pictures") && !b.id.endsWith("_pictures")) { return 1; }
150
+ if(!a.id.endsWith("_pictures") && b.id.endsWith("_pictures")) { return -1; }
151
+ else { return 0; }
152
+ }
153
+ }
154
+ });
155
+
156
+ return style;
157
+ }
158
+
159
+ /**
160
+ * Identifies missing layers for a complete rendering of GeoVisio vector tiles.
161
+ * This allows retro-compatibility with GeoVisio instances <= 2.5.0
162
+ * which didn't offer a MapLibre JSON style directly.
163
+ *
164
+ * @param {object} sources Pre-existing MapLibre style sources
165
+ * @param {object} layers Pre-existing MapLibre style layers
166
+ * @returns List of layers to add
167
+ * @private
168
+ */
169
+ export function getMissingLayerStyles(sources, layers) {
170
+ const newLayers = [];
171
+
172
+ // GeoVisio API <= 2.5.0 : add sequences + pictures
173
+ Object.keys(sources).filter(s => (
174
+ layers.find(l => l?.source === s) === undefined
175
+ )).forEach(s => {
176
+ if(s.startsWith("geovisio")) {
177
+ // Basic sequences
178
+ newLayers.push({
179
+ "id": `${s}_sequences`,
180
+ "type": "line",
181
+ "source": s,
182
+ "source-layer": "sequences",
183
+ "layout": {
184
+ ...VECTOR_STYLES.SEQUENCES.layout
185
+ },
186
+ "paint": {
187
+ ...VECTOR_STYLES.SEQUENCES.paint,
188
+ "line-color": COLORS.BASE,
189
+ },
190
+ });
191
+
192
+ // Padded sequence (for easier click)
193
+ newLayers.push({
194
+ "id": `${s}_sequences_plus`,
195
+ "type": "line",
196
+ "source": s,
197
+ "source-layer": "sequences",
198
+ "layout": {
199
+ ...VECTOR_STYLES.SEQUENCES_PLUS.layout
200
+ },
201
+ "paint": {
202
+ ...VECTOR_STYLES.SEQUENCES_PLUS.paint
203
+ },
204
+ });
205
+
206
+ // Pictures symbols
207
+ newLayers.push({
208
+ "id": `${s}_pictures_symbols`,
209
+ "type": "symbol",
210
+ "source": s,
211
+ "source-layer": "pictures",
212
+ ...VECTOR_STYLES.PICTURES_SYMBOLS,
213
+ });
214
+
215
+ // Pictures symbols
216
+ newLayers.push({
217
+ "id": `${s}_pictures_symbols`,
218
+ "type": "symbol",
219
+ "source": s,
220
+ "source-layer": "pictures",
221
+ ...VECTOR_STYLES.PICTURES_SYMBOLS,
222
+ });
223
+
224
+ // Pictures
225
+ newLayers.push({
226
+ "id": `${s}_pictures`,
227
+ "type": "circle",
228
+ "source": s,
229
+ "source-layer": "pictures",
230
+ "layout": {
231
+ ...VECTOR_STYLES.PICTURES.layout
232
+ },
233
+ "paint": {
234
+ ...VECTOR_STYLES.PICTURES.paint,
235
+ "circle-color": COLORS.BASE,
236
+ },
237
+ });
238
+ }
239
+ });
240
+
241
+ // Add sequences_plus for easier click on map
242
+ layers.filter(l => (
243
+ l?.id?.endsWith("_sequences")
244
+ && layers.find(sl => sl?.id === l.id+"_plus") === undefined
245
+ )).forEach(l => {
246
+ newLayers.push({
247
+ "id": `${l.id}_plus`,
248
+ "type": "line",
249
+ "source": l.source,
250
+ "source-layer": l["source-layer"],
251
+ "layout": {
252
+ ...VECTOR_STYLES.SEQUENCES_PLUS.layout
253
+ },
254
+ "paint": {
255
+ ...VECTOR_STYLES.SEQUENCES_PLUS.paint
256
+ },
257
+ });
258
+ });
259
+
260
+ // Add pictures symbol for high-level zooms
261
+ layers.filter(l => (
262
+ l?.id?.endsWith("_pictures")
263
+ && layers.find(sl => sl?.id === l.id+"_symbols") === undefined
264
+ )).forEach(l => {
265
+ // Symbols
266
+ newLayers.unshift({
267
+ "id": `${l.id}_symbols`,
268
+ "type": "symbol",
269
+ "source": l.source,
270
+ "source-layer": "pictures",
271
+ ...VECTOR_STYLES.PICTURES_SYMBOLS,
272
+ });
273
+
274
+ // Patch style of pictures layer
275
+ l.paint = Object.assign(l.paint || {}, VECTOR_STYLES.PICTURES.paint);
276
+ l.layout = Object.assign(l.layout || {}, VECTOR_STYLES.PICTURES.layout);
277
+ });
278
+
279
+
280
+ return newLayers;
281
+ }
282
+
283
+ /**
284
+ * Get cleaned-up layer ID for a specific user.
285
+ * @param {string} userId The user UUID (or "geovisio" for general layer)
286
+ * @param {string} layerType The kind of layer (pictures, sequences...)
287
+ * @returns {string} The cleaned-up layer ID for MapLibre
288
+ * @private
289
+ */
290
+ export function getUserLayerId(userId, layerType) {
291
+ return `${getUserSourceId(userId)}_${layerType}`;
292
+ }
293
+
294
+ /**
295
+ * Get cleaned-up source ID for a specific user.
296
+ * @param {string} userId The user UUID (or "geovisio" for general layer)
297
+ * @returns {string} The cleaned-up source ID for MapLibre
298
+ * @private
299
+ */
300
+ export function getUserSourceId(userId) {
301
+ return userId === "geovisio" ? "geovisio" : "geovisio_"+userId;
302
+ }
303
+
304
+ /**
305
+ * Transforms a set of parameters into an URL-ready string
306
+ * It also removes null/undefined values
307
+ *
308
+ * @param {object} params The parameters object
309
+ * @return {string} The URL query part
310
+ * @private
311
+ */
312
+ export function geocoderParamsToURLString(params) {
313
+ let p = {};
314
+ Object.entries(params)
315
+ .filter(e => e[1] !== undefined && e[1] !== null)
316
+ .forEach(e => p[e[0]] = e[1]);
317
+
318
+ return new URLSearchParams(p).toString();
319
+ }
320
+
321
+ /**
322
+ * Nominatim (OSM) geocoder, ready to use for our Map
323
+ * @private
324
+ */
325
+ export function forwardGeocodingNominatim(config) {
326
+ // Transform parameters into Nominatim format
327
+ const params = {
328
+ q: config.query,
329
+ countrycodes: config.countries,
330
+ limit: config.limit,
331
+ viewbox: config.bbox,
332
+ };
333
+
334
+ return fetch(`https://nominatim.openstreetmap.org/search?${geocoderParamsToURLString(params)}&format=geojson&polygon_geojson=1&addressdetails=1`)
335
+ .then(res => res.json())
336
+ .then(res => {
337
+ const finalRes = { features: [] };
338
+ const listedNames = [];
339
+ res.features.forEach(f => {
340
+ if(!listedNames.includes(f.properties.display_name)) {
341
+ finalRes.features.push({
342
+ place_type: ["place"],
343
+ place_name: f.properties.display_name,
344
+ bounds: new maplibregl.LngLatBounds(f.bbox),
345
+ });
346
+ listedNames.push(f.properties.display_name);
347
+ }
348
+ });
349
+ return finalRes;
350
+ });
351
+ }
352
+
353
+ /**
354
+ * Base adresse nationale (FR) geocoder, ready to use for our Map
355
+ * @param {object} config Configuration sent by MapLibre GL Geocoder, following the geocoderApi format ( https://github.com/maplibre/maplibre-gl-geocoder/blob/main/API.md#setgeocoderapi )
356
+ * @returns {object} GeoJSON Feature collection in Carmen GeoJSON format
357
+ * @private
358
+ */
359
+ export function forwardGeocodingBAN(config) {
360
+ // Transform parameters into BAN format
361
+ const params = { q: config.query, limit: config.limit };
362
+ if(typeof config.proximity === "string") {
363
+ const [lat, lon] = config.proximity.split(",").map(v => parseFloat(v.trim()));
364
+ params.lat = lat;
365
+ params.lon = lon;
366
+ }
367
+
368
+ const toPlaceName = p => [p.name, p.district, p.city].filter(v => v).join(", ");
369
+ const placeTypeToZoom = { "housenumber": 20, "street": 18, "locality": 15, "municipality": 12 };
370
+
371
+ return fetch(`https://api-adresse.data.gouv.fr/search/?${geocoderParamsToURLString(params)}`)
372
+ .then(res => res.json())
373
+ .then(res => {
374
+ res.features = res.features.map(f => ({
375
+ place_type: ["place"],
376
+ place_name: toPlaceName(f.properties),
377
+ center: new maplibregl.LngLat(...f.geometry.coordinates),
378
+ zoom: placeTypeToZoom[f.properties.type],
379
+ }));
380
+ return res;
381
+ });
382
+ }
@@ -0,0 +1,45 @@
1
+ import { EquirectangularTilesAdapter } from "@photo-sphere-viewer/equirectangular-tiles-adapter";
2
+
3
+ /**
4
+ * Override of PSV EquirectangularTilesAdapter for fine-tweaking.
5
+ * @private
6
+ */
7
+ export default class PhotoAdapter extends EquirectangularTilesAdapter {
8
+ constructor(viewer, config) {
9
+ super(viewer, config);
10
+ this._shouldGoFast = config.shouldGoFast || (() => true);
11
+ }
12
+
13
+ /**
14
+ * Override to skip loading SD images according to shouldGoFast option.
15
+ */
16
+ loadTexture(panorama, loader) {
17
+ if(!panorama.origBaseUrl) { panorama.origBaseUrl = panorama.baseUrl; }
18
+ else { panorama.baseUrl = panorama.origBaseUrl; }
19
+
20
+ // Fast mode + thumbnail available + no HD image loaded yet + flat picture
21
+ if(
22
+ this._shouldGoFast()
23
+ && panorama.thumbUrl
24
+ && !panorama.hdLoaded
25
+ && panorama.rows == 1
26
+ ) {
27
+ panorama.baseUrl = panorama.thumbUrl;
28
+ }
29
+
30
+ return super.loadTexture(panorama, loader).then(data => {
31
+ if(panorama.baseUrl === panorama.origBaseUrl) {
32
+ panorama.hdLoaded = true;
33
+ }
34
+ return data;
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Override to skip loading tiles according to shouldGoFast option.
40
+ * @private
41
+ */
42
+ __loadTiles(tiles) {
43
+ if(!this._shouldGoFast()) { super.__loadTiles(tiles); }
44
+ }
45
+ }