@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.
- package/.dockerignore +6 -0
- package/.gitlab-ci.yml +71 -0
- package/CHANGELOG.md +428 -0
- package/CODE_OF_CONDUCT.md +134 -0
- package/Dockerfile +14 -0
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/build/editor.html +1 -0
- package/build/index.css +36 -0
- package/build/index.css.map +1 -0
- package/build/index.html +1 -0
- package/build/index.js +25 -0
- package/build/index.js.map +1 -0
- package/build/map.html +1 -0
- package/build/viewer.html +1 -0
- package/config/env.js +104 -0
- package/config/getHttpsConfig.js +66 -0
- package/config/getPackageJson.js +25 -0
- package/config/jest/babelTransform.js +29 -0
- package/config/jest/cssTransform.js +14 -0
- package/config/jest/fileTransform.js +40 -0
- package/config/modules.js +134 -0
- package/config/paths.js +72 -0
- package/config/pnpTs.js +35 -0
- package/config/webpack/persistentCache/createEnvironmentHash.js +9 -0
- package/config/webpack.config.js +885 -0
- package/config/webpackDevServer.config.js +127 -0
- package/docs/01_Start.md +149 -0
- package/docs/02_Usage.md +828 -0
- package/docs/03_URL_settings.md +140 -0
- package/docs/04_Advanced_examples.md +214 -0
- package/docs/05_Compatibility.md +85 -0
- package/docs/09_Develop.md +62 -0
- package/docs/90_Releases.md +27 -0
- package/docs/images/class_diagram.drawio +129 -0
- package/docs/images/class_diagram.jpg +0 -0
- package/docs/images/screenshot.jpg +0 -0
- package/mkdocs.yml +45 -0
- package/package.json +254 -0
- package/public/editor.html +54 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +59 -0
- package/public/map.html +53 -0
- package/public/viewer.html +67 -0
- package/scripts/build.js +217 -0
- package/scripts/start.js +176 -0
- package/scripts/test.js +52 -0
- package/src/Editor.css +37 -0
- package/src/Editor.js +359 -0
- package/src/StandaloneMap.js +114 -0
- package/src/Viewer.css +203 -0
- package/src/Viewer.js +1186 -0
- package/src/components/CoreView.css +64 -0
- package/src/components/CoreView.js +159 -0
- package/src/components/Loader.css +56 -0
- package/src/components/Loader.js +111 -0
- package/src/components/Map.css +65 -0
- package/src/components/Map.js +841 -0
- package/src/components/Photo.css +36 -0
- package/src/components/Photo.js +687 -0
- package/src/img/arrow_360.svg +14 -0
- package/src/img/arrow_flat.svg +11 -0
- package/src/img/arrow_triangle.svg +10 -0
- package/src/img/arrow_turn.svg +9 -0
- package/src/img/bg_aerial.jpg +0 -0
- package/src/img/bg_streets.jpg +0 -0
- package/src/img/loader_base.jpg +0 -0
- package/src/img/loader_hd.jpg +0 -0
- package/src/img/logo_dead.svg +91 -0
- package/src/img/marker.svg +17 -0
- package/src/img/marker_blue.svg +20 -0
- package/src/img/switch_big.svg +44 -0
- package/src/img/switch_mini.svg +48 -0
- package/src/index.js +10 -0
- package/src/translations/de.json +163 -0
- package/src/translations/en.json +164 -0
- package/src/translations/eo.json +6 -0
- package/src/translations/es.json +164 -0
- package/src/translations/fi.json +1 -0
- package/src/translations/fr.json +164 -0
- package/src/translations/hu.json +133 -0
- package/src/translations/nl.json +1 -0
- package/src/translations/zh_Hant.json +136 -0
- package/src/utils/API.js +709 -0
- package/src/utils/Exif.js +198 -0
- package/src/utils/I18n.js +75 -0
- package/src/utils/Map.js +382 -0
- package/src/utils/PhotoAdapter.js +45 -0
- package/src/utils/Utils.js +568 -0
- package/src/utils/Widgets.js +477 -0
- package/src/viewer/URLHash.js +334 -0
- package/src/viewer/Widgets.css +711 -0
- package/src/viewer/Widgets.js +1196 -0
- package/tests/Editor.test.js +125 -0
- package/tests/StandaloneMap.test.js +44 -0
- package/tests/Viewer.test.js +363 -0
- package/tests/__snapshots__/Editor.test.js.snap +300 -0
- package/tests/__snapshots__/StandaloneMap.test.js.snap +30 -0
- package/tests/__snapshots__/Viewer.test.js.snap +195 -0
- package/tests/components/CoreView.test.js +91 -0
- package/tests/components/Loader.test.js +38 -0
- package/tests/components/Map.test.js +230 -0
- package/tests/components/Photo.test.js +335 -0
- package/tests/components/__snapshots__/Loader.test.js.snap +15 -0
- package/tests/components/__snapshots__/Map.test.js.snap +767 -0
- package/tests/components/__snapshots__/Photo.test.js.snap +205 -0
- package/tests/data/Map_geocoder_ban.json +36 -0
- package/tests/data/Map_geocoder_nominatim.json +56 -0
- package/tests/data/Viewer_pictures_1.json +148 -0
- package/tests/setupTests.js +5 -0
- package/tests/utils/API.test.js +906 -0
- package/tests/utils/Exif.test.js +124 -0
- package/tests/utils/I18n.test.js +28 -0
- package/tests/utils/Map.test.js +105 -0
- package/tests/utils/Utils.test.js +300 -0
- package/tests/utils/Widgets.test.js +107 -0
- package/tests/utils/__snapshots__/API.test.js.snap +132 -0
- package/tests/utils/__snapshots__/Exif.test.js.snap +43 -0
- package/tests/utils/__snapshots__/Map.test.js.snap +48 -0
- package/tests/utils/__snapshots__/Utils.test.js.snap +41 -0
- package/tests/utils/__snapshots__/Widgets.test.js.snap +44 -0
- package/tests/viewer/URLHash.test.js +537 -0
- package/tests/viewer/Widgets.test.js +127 -0
- package/tests/viewer/__snapshots__/URLHash.test.js.snap +98 -0
- 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
|
+
}
|
package/src/utils/Map.js
ADDED
|
@@ -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
|
+
}
|