@panoramax/web-viewer 3.2.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitlab-ci.yml +13 -6
- package/CHANGELOG.md +49 -1
- package/CODE_OF_CONDUCT.md +1 -1
- package/README.md +1 -1
- package/build/editor.html +10 -1
- package/build/index.css +12 -12
- package/build/index.css.map +1 -1
- package/build/index.html +1 -1
- package/build/index.js +2126 -14
- package/build/index.js.map +1 -1
- package/build/map.html +1 -1
- package/build/photo.html +1 -0
- package/build/static/media/atkinson-hyperlegible-next-latin-400-normal..woff +0 -0
- package/build/static/media/atkinson-hyperlegible-next-latin-400-normal..woff2 +0 -0
- package/build/static/media/atkinson-hyperlegible-next-latin-ext-400-normal..woff +0 -0
- package/build/static/media/atkinson-hyperlegible-next-latin-ext-400-normal..woff2 +0 -0
- package/build/viewer.html +12 -1
- package/build/widgets.html +1 -0
- package/config/jest/mocks.js +201 -0
- package/config/paths.js +2 -0
- package/config/webpack.config.js +52 -0
- package/docs/03_URL_settings.md +14 -16
- package/docs/05_Compatibility.md +59 -76
- package/docs/09_Develop.md +46 -11
- package/docs/90_Releases.md +2 -2
- package/docs/images/class_diagram.drawio +60 -45
- package/docs/images/class_diagram.jpg +0 -0
- package/docs/images/screenshot.jpg +0 -0
- package/docs/index.md +135 -0
- package/docs/reference/components/core/Basic.md +196 -0
- package/docs/reference/components/core/CoverageMap.md +210 -0
- package/docs/reference/components/core/Editor.md +224 -0
- package/docs/reference/components/core/PhotoViewer.md +307 -0
- package/docs/reference/components/core/Viewer.md +350 -0
- package/docs/reference/components/layout/BottomDrawer.md +35 -0
- package/docs/reference/components/layout/CorneredGrid.md +29 -0
- package/docs/reference/components/layout/Mini.md +45 -0
- package/docs/reference/components/layout/Tabs.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 +16 -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 +40 -0
- package/docs/reference/components/ui/ButtonGroup.md +36 -0
- package/docs/reference/components/ui/CopyButton.md +38 -0
- package/docs/reference/components/ui/Grade.md +32 -0
- package/docs/reference/components/ui/LinkButton.md +45 -0
- package/docs/reference/components/ui/ListGroup.md +22 -0
- package/docs/reference/components/ui/Loader.md +56 -0
- package/docs/reference/components/ui/Map.md +239 -0
- package/docs/reference/components/ui/MapMore.md +256 -0
- package/docs/reference/components/ui/Photo.md +385 -0
- package/docs/reference/components/ui/Popup.md +56 -0
- package/docs/reference/components/ui/ProgressBar.md +32 -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 +49 -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/OSMEditors.md +15 -0
- package/docs/reference/components/ui/widgets/PictureLegendActions.md +32 -0
- package/docs/reference/components/ui/widgets/Player.md +33 -0
- package/docs/reference/components/ui/widgets/Zoom.md +15 -0
- package/docs/reference/utils/API.md +334 -0
- package/docs/reference/utils/InitParameters.md +68 -0
- package/docs/reference/utils/URLHandler.md +107 -0
- package/docs/reference.md +79 -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 +59 -0
- package/docs/tutorials/map_style.md +39 -0
- package/docs/tutorials/migrate_v4.md +153 -0
- package/docs/tutorials/synced_coverage.md +43 -0
- package/mkdocs.yml +66 -5
- package/package.json +22 -17
- package/public/editor.html +21 -29
- package/public/index.html +17 -12
- package/public/map.html +19 -18
- package/public/photo.html +55 -0
- package/public/viewer.html +22 -26
- package/public/widgets.html +306 -0
- package/scripts/doc.js +79 -0
- package/src/components/core/Basic.css +48 -0
- package/src/components/core/Basic.js +349 -0
- package/src/components/core/CoverageMap.css +9 -0
- package/src/components/core/CoverageMap.js +139 -0
- package/src/components/core/Editor.css +23 -0
- package/src/components/core/Editor.js +390 -0
- package/src/components/core/PhotoViewer.css +48 -0
- package/src/components/core/PhotoViewer.js +499 -0
- package/src/components/core/Viewer.css +98 -0
- package/src/components/core/Viewer.js +564 -0
- package/src/components/core/index.js +12 -0
- package/src/components/index.js +13 -0
- package/src/components/layout/BottomDrawer.js +257 -0
- package/src/components/layout/CorneredGrid.js +112 -0
- package/src/components/layout/Mini.js +117 -0
- package/src/components/layout/Tabs.js +133 -0
- package/src/components/layout/index.js +9 -0
- package/src/components/menus/MapBackground.js +106 -0
- package/src/components/menus/MapFilters.js +400 -0
- package/src/components/menus/MapLayers.js +143 -0
- package/src/components/menus/MapLegend.js +34 -0
- package/src/components/menus/PictureLegend.js +253 -0
- package/src/components/menus/PictureMetadata.js +317 -0
- package/src/components/menus/PlayerOptions.js +95 -0
- package/src/components/menus/QualityScoreDoc.js +36 -0
- package/src/components/menus/ReportForm.js +133 -0
- package/src/components/menus/Share.js +100 -0
- package/src/components/menus/index.js +15 -0
- package/src/components/styles.js +383 -0
- package/src/components/ui/Button.js +77 -0
- package/src/components/ui/ButtonGroup.css +57 -0
- package/src/components/ui/ButtonGroup.js +68 -0
- package/src/components/ui/CopyButton.js +106 -0
- package/src/components/ui/Grade.js +54 -0
- package/src/components/ui/LinkButton.js +67 -0
- package/src/components/ui/ListGroup.js +66 -0
- package/src/components/ui/Loader.js +203 -0
- package/src/components/{Map.css → ui/Map.css} +5 -17
- package/src/components/{Map.js → ui/Map.js} +148 -156
- 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} +313 -101
- package/src/components/ui/Popup.js +145 -0
- package/src/components/ui/ProgressBar.js +104 -0
- package/src/components/ui/QualityScore.js +147 -0
- package/src/components/ui/SearchBar.js +367 -0
- package/src/components/ui/TogglableGroup.js +157 -0
- package/src/components/ui/index.js +22 -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 +113 -0
- package/src/components/ui/widgets/MapFiltersButton.js +104 -0
- package/src/components/ui/widgets/MapLayersButton.js +79 -0
- package/src/components/ui/widgets/OSMEditors.js +155 -0
- package/src/components/ui/widgets/PictureLegendActions.js +117 -0
- package/src/components/ui/widgets/Player.css +7 -0
- package/src/components/ui/widgets/Player.js +151 -0
- package/src/components/ui/widgets/Zoom.js +82 -0
- package/src/components/ui/widgets/index.js +13 -0
- package/src/img/loader_base.jpg +0 -0
- package/src/img/panoramax.svg +13 -0
- package/src/img/switch_big.svg +20 -10
- package/src/index.js +7 -9
- package/src/translations/br.json +1 -0
- package/src/translations/da.json +38 -15
- package/src/translations/de.json +5 -3
- package/src/translations/en.json +35 -15
- package/src/translations/eo.json +38 -15
- package/src/translations/es.json +1 -1
- package/src/translations/fr.json +36 -16
- package/src/translations/hu.json +1 -1
- package/src/translations/it.json +39 -16
- package/src/translations/ja.json +182 -1
- package/src/translations/nl.json +106 -6
- package/src/translations/pl.json +1 -1
- package/src/translations/sv.json +182 -0
- package/src/translations/zh_Hant.json +35 -14
- package/src/utils/API.js +109 -49
- package/src/utils/InitParameters.js +388 -0
- package/src/utils/PhotoAdapter.js +1 -0
- package/src/utils/URLHandler.js +362 -0
- package/src/utils/geocoder.js +152 -0
- package/src/utils/{I18n.js → i18n.js} +7 -3
- package/src/utils/index.js +11 -0
- package/src/utils/{Map.js → map.js} +256 -77
- package/src/utils/picture.js +442 -0
- package/src/utils/utils.js +324 -0
- package/src/utils/widgets.js +55 -0
- package/tests/components/core/Basic.test.js +121 -0
- package/tests/components/core/BasicMock.js +25 -0
- package/tests/components/core/CoverageMap.test.js +20 -0
- package/tests/components/core/Editor.test.js +20 -0
- package/tests/components/core/PhotoViewer.test.js +57 -0
- package/tests/components/core/Viewer.test.js +84 -0
- package/tests/components/core/__snapshots__/PhotoViewer.test.js.snap +73 -0
- package/tests/components/core/__snapshots__/Viewer.test.js.snap +145 -0
- package/tests/components/ui/CopyButton.test.js +52 -0
- package/tests/components/ui/Loader.test.js +55 -0
- package/tests/components/{Map.test.js → ui/Map.test.js} +73 -61
- package/tests/components/{Photo.test.js → ui/Photo.test.js} +97 -63
- package/tests/components/ui/Popup.test.js +26 -0
- package/tests/components/ui/QualityScore.test.js +18 -0
- package/tests/components/ui/SearchBar.test.js +110 -0
- package/tests/components/ui/__snapshots__/CopyButton.test.js.snap +33 -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 +70 -6
- 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 +83 -83
- package/tests/utils/InitParameters.test.js +499 -0
- package/tests/utils/URLHandler.test.js +401 -0
- package/tests/utils/__snapshots__/API.test.js.snap +10 -0
- package/tests/utils/__snapshots__/URLHandler.test.js.snap +21 -0
- package/tests/utils/__snapshots__/{Map.test.js.snap → geocoder.test.js.snap} +1 -1
- package/tests/utils/__snapshots__/map.test.js.snap +11 -0
- package/tests/utils/__snapshots__/picture.test.js.snap +327 -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} +8 -8
- package/tests/utils/map.test.js +126 -0
- package/tests/utils/picture.test.js +745 -0
- package/tests/utils/utils.test.js +288 -0
- package/tests/utils/widgets.test.js +31 -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/img/loader_hd.jpg +0 -0
- 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
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import ArrowTriangleSVG from "../img/arrow_triangle.svg";
|
|
2
|
+
import ArrowTurnSVG from "../img/arrow_turn.svg";
|
|
3
|
+
import { svgToPSVLink, COLORS, getDistance, getSimplifiedAngle, getArrow } from "./utils";
|
|
4
|
+
|
|
5
|
+
const ArrowTriangle = svgToPSVLink(ArrowTriangleSVG, "white");
|
|
6
|
+
const ArrowTurn = svgToPSVLink(ArrowTurnSVG, COLORS.NEXT);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read float value from EXIF tags (to handle fractions & all)
|
|
10
|
+
* @param {*} val The input EXIF tag value
|
|
11
|
+
* @returns {number|undefined} The parsed value, or undefined if value is not readable
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
export function getExifFloat(val) {
|
|
15
|
+
// Null-like values
|
|
16
|
+
if(
|
|
17
|
+
[null, undefined, ""].includes(val)
|
|
18
|
+
|| typeof val === "string" && val.trim() === ""
|
|
19
|
+
) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
// Already valid number
|
|
23
|
+
else if(typeof val === "number") {
|
|
24
|
+
return val;
|
|
25
|
+
}
|
|
26
|
+
// String
|
|
27
|
+
else if(typeof val === "string") {
|
|
28
|
+
// Check if looks like a fraction
|
|
29
|
+
if(/^-?\d+(\.\d+)?\/-?\d+(\.\d+)?$/.test(val)) {
|
|
30
|
+
const parts = val.split("/").map(p => parseFloat(p));
|
|
31
|
+
return parts[0] / parts[1];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Try a direct cast to float
|
|
35
|
+
try { return parseFloat(val); }
|
|
36
|
+
catch(e) {} // eslint-disable-line no-empty
|
|
37
|
+
|
|
38
|
+
// Unrecognized
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
else { return undefined; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Find in picture metadata the GPS precision.
|
|
46
|
+
* @param {object} picture The GeoJSON picture feature
|
|
47
|
+
* @returns {string} The precision value (poor, fair, moderate, good, excellent, ideal, unknown)
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
export function getGPSPrecision(picture) {
|
|
51
|
+
let quality = null;
|
|
52
|
+
const gpsHPosError = picture?.properties?.["quality:horizontal_accuracy"] || getExifFloat(picture?.properties?.exif?.["Exif.GPSInfo.GPSHPositioningError"]);
|
|
53
|
+
const gpsDop = getExifFloat(picture?.properties?.exif?.["Exif.GPSInfo.GPSDOP"]);
|
|
54
|
+
|
|
55
|
+
if(gpsHPosError !== undefined) {
|
|
56
|
+
quality = `${gpsHPosError} m`;
|
|
57
|
+
}
|
|
58
|
+
else if(gpsDop !== undefined) {
|
|
59
|
+
if(gpsDop < 1) { quality = "ideal"; }
|
|
60
|
+
else if(gpsDop < 2) { quality = "excellent"; }
|
|
61
|
+
else if(gpsDop < 5) { quality = "good"; }
|
|
62
|
+
else if(gpsDop < 10) { quality = "moderate"; }
|
|
63
|
+
else if(gpsDop < 20) { quality = "fair"; }
|
|
64
|
+
else { quality = "poor"; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return quality;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Compute PSV sphere correction based on picture metadata & EXIF tags.
|
|
72
|
+
* @param {object} picture The GeoJSON picture feature
|
|
73
|
+
* @returns {object} The PSV sphereCorrection value
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
export function getSphereCorrection(picture) {
|
|
77
|
+
// Photo direction
|
|
78
|
+
let dir = picture.properties?.["view:azimuth"];
|
|
79
|
+
if(dir === undefined) {
|
|
80
|
+
const v = getExifFloat(picture.properties?.exif?.["Exif.GPSInfo.GPSImgDirection"]);
|
|
81
|
+
if(v !== undefined) {
|
|
82
|
+
dir = v;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
dir = dir || 0;
|
|
86
|
+
|
|
87
|
+
// Yaw
|
|
88
|
+
let yaw = picture.properties?.["pers:yaw"];
|
|
89
|
+
let exifFallbacks = ["Xmp.GPano.PoseHeadingDegrees", "Xmp.Camera.Yaw", "Exif.MpfInfo.MPFYawAngle"];
|
|
90
|
+
if(yaw === undefined) {
|
|
91
|
+
for(let exif of exifFallbacks) {
|
|
92
|
+
const v = getExifFloat(picture.properties?.exif?.[exif]);
|
|
93
|
+
if(v !== undefined) {
|
|
94
|
+
yaw = v;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
yaw = yaw || 0;
|
|
100
|
+
|
|
101
|
+
// Check if yaw is applicable: different from photo direction
|
|
102
|
+
if(Math.round(dir) === Math.round(yaw) && yaw > 0) {
|
|
103
|
+
console.warn("Picture with UUID", picture.id, "has same GPS Image direction and Yaw, could cause rendering issues");
|
|
104
|
+
// yaw = 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Pitch
|
|
108
|
+
let pitch = picture.properties?.["pers:pitch"];
|
|
109
|
+
exifFallbacks = ["Xmp.GPano.PosePitchDegrees", "Xmp.Camera.Pitch", "Exif.MpfInfo.MPFPitchAngle"];
|
|
110
|
+
if(pitch === undefined) {
|
|
111
|
+
for(let exif of exifFallbacks) {
|
|
112
|
+
const v = getExifFloat(picture.properties?.exif?.[exif]);
|
|
113
|
+
if(v !== undefined) {
|
|
114
|
+
pitch = v;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
pitch = pitch || 0;
|
|
120
|
+
|
|
121
|
+
// Roll
|
|
122
|
+
let roll = picture.properties?.["pers:roll"];
|
|
123
|
+
exifFallbacks = ["Xmp.GPano.PoseRollDegrees", "Xmp.Camera.Roll", "Exif.MpfInfo.MPFRollAngle"];
|
|
124
|
+
if(roll === undefined) {
|
|
125
|
+
for(let exif of exifFallbacks) {
|
|
126
|
+
const v = getExifFloat(picture.properties?.exif?.[exif]);
|
|
127
|
+
if(v !== undefined) {
|
|
128
|
+
roll = v;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
roll = roll || 0;
|
|
134
|
+
|
|
135
|
+
// Send result
|
|
136
|
+
return pitch !== 0 && roll !== 0 ? {
|
|
137
|
+
pan: yaw * Math.PI / 180,
|
|
138
|
+
tilt: pitch * Math.PI / 180,
|
|
139
|
+
roll: roll * Math.PI / 180,
|
|
140
|
+
} : {};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Compute PSV panoData for cropped panorama based on picture metadata & EXIF tags.
|
|
145
|
+
* @param {object} picture The GeoJSON picture feature
|
|
146
|
+
* @returns {object} The PSV panoData values
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
export function getCroppedPanoData(picture) {
|
|
150
|
+
let res;
|
|
151
|
+
|
|
152
|
+
if(picture.properties?.["pers:interior_orientation"]) {
|
|
153
|
+
if(
|
|
154
|
+
picture.properties["pers:interior_orientation"]?.["visible_area"]
|
|
155
|
+
&& picture.properties["pers:interior_orientation"]?.["sensor_array_dimensions"]
|
|
156
|
+
) {
|
|
157
|
+
const va = picture.properties["pers:interior_orientation"]["visible_area"];
|
|
158
|
+
const sad = picture.properties["pers:interior_orientation"]["sensor_array_dimensions"];
|
|
159
|
+
try {
|
|
160
|
+
res = {
|
|
161
|
+
fullWidth: parseInt(sad[0]),
|
|
162
|
+
fullHeight: parseInt(sad[1]),
|
|
163
|
+
croppedX: parseInt(va[0]),
|
|
164
|
+
croppedY: parseInt(va[1]),
|
|
165
|
+
croppedWidth: parseInt(sad[0]) - parseInt(va[2]) - parseInt(va[0]),
|
|
166
|
+
croppedHeight: parseInt(sad[1]) - parseInt(va[3]) - parseInt(va[1]),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
catch(e) {
|
|
170
|
+
console.warn("Invalid pers:interior_orientation values for cropped panorama "+picture.id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if(!res && picture.properties?.exif) {
|
|
176
|
+
try {
|
|
177
|
+
res = {
|
|
178
|
+
fullWidth: parseInt(picture.properties.exif?.["Xmp.GPano.FullPanoWidthPixels"]),
|
|
179
|
+
fullHeight: parseInt(picture.properties.exif?.["Xmp.GPano.FullPanoHeightPixels"]),
|
|
180
|
+
croppedX: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaLeftPixels"]),
|
|
181
|
+
croppedY: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaTopPixels"]),
|
|
182
|
+
croppedWidth: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaImageWidthPixels"]),
|
|
183
|
+
croppedHeight: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaImageHeightPixels"]),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
catch(e) {
|
|
187
|
+
console.warn("Invalid XMP.GPano values for cropped panorama "+picture.id);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check if crop is really necessary
|
|
192
|
+
if(res) {
|
|
193
|
+
res = Object.fromEntries(Object.entries(res || {}).filter(e => !isNaN(e[1])));
|
|
194
|
+
if(res.fullWidth == res.croppedWidth && res.fullHeight == res.croppedHeight) {
|
|
195
|
+
res = {};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return res || {};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Compare function to retrieve most appropriate picture in a single direction.
|
|
204
|
+
*
|
|
205
|
+
* @param {number[]} picPos The picture [x,y] position
|
|
206
|
+
* @returns {function} A compare function for sorting
|
|
207
|
+
* @private
|
|
208
|
+
*/
|
|
209
|
+
export function sortPicturesInDirection(picPos) {
|
|
210
|
+
return (a,b) => {
|
|
211
|
+
// Two prev/next links = no sort
|
|
212
|
+
if(a.rel != "related" && b.rel != "related") { return 0; }
|
|
213
|
+
// First is prev/next link = goes first
|
|
214
|
+
else if(a.rel != "related") { return -1; }
|
|
215
|
+
// Second is prev/next link = goes first
|
|
216
|
+
else if(b.rel != "related") { return 1; }
|
|
217
|
+
// Two related links same day = nearest goes first
|
|
218
|
+
else if(a.date == b.date) { return getDistance(picPos, a.geometry.coordinates) - getDistance(picPos, b.geometry.coordinates); }
|
|
219
|
+
// Two related links at different day = recent goes first
|
|
220
|
+
else { return b.date.localeCompare(a.date); }
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Generates the navbar caption based on a single picture metadata
|
|
226
|
+
*
|
|
227
|
+
* @param {object} metadata The picture metadata
|
|
228
|
+
* @param {object} t The labels translations container
|
|
229
|
+
* @returns {object} Normalized object with user name, licence and date
|
|
230
|
+
* @private
|
|
231
|
+
*/
|
|
232
|
+
export function getNodeCaption(metadata, t) {
|
|
233
|
+
const caption = {};
|
|
234
|
+
|
|
235
|
+
// Timestamp
|
|
236
|
+
if(metadata?.properties?.datetimetz) {
|
|
237
|
+
caption.date = new Date(metadata.properties.datetimetz);
|
|
238
|
+
const timeZoneMatch = metadata.properties.datetimetz.match(/([+-]\d{2}):(\d{2})$|Z$/);
|
|
239
|
+
if (timeZoneMatch) {
|
|
240
|
+
if (timeZoneMatch[0] === "Z") {
|
|
241
|
+
caption.tz = "UTC";
|
|
242
|
+
} else {
|
|
243
|
+
caption.tz = timeZoneMatch[0];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if(metadata?.properties?.datetime) {
|
|
248
|
+
caption.date = new Date(metadata.properties.datetime);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Producer
|
|
252
|
+
if(metadata?.providers) {
|
|
253
|
+
const producerRoles = metadata?.providers?.filter(el => el?.roles?.includes("producer"));
|
|
254
|
+
if(producerRoles?.length >= 0) {
|
|
255
|
+
// Avoid duplicates between account name and picture author
|
|
256
|
+
const producersDeduped = {};
|
|
257
|
+
producerRoles.map(p => p.name).forEach(p => {
|
|
258
|
+
const pmin = p.toLowerCase().replace(/\s/g, "");
|
|
259
|
+
if(producersDeduped[pmin]) { producersDeduped[pmin].push(p); }
|
|
260
|
+
else { producersDeduped[pmin] = [p];}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Keep best looking name for each
|
|
264
|
+
caption.producer = [];
|
|
265
|
+
Object.values(producersDeduped).forEach(pv => {
|
|
266
|
+
const deflt = pv[0];
|
|
267
|
+
const better = pv.find(v => v.toLowerCase() != v);
|
|
268
|
+
caption.producer.push(better || deflt);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// License
|
|
274
|
+
if(metadata?.properties?.license) {
|
|
275
|
+
caption.license = metadata.properties.license;
|
|
276
|
+
// Look for URL to license
|
|
277
|
+
if(metadata?.links) {
|
|
278
|
+
const licenseLink = metadata.links.find(l => l?.rel === "license");
|
|
279
|
+
if(licenseLink) {
|
|
280
|
+
caption.license = `<a href="${licenseLink.href}" title="${t.pnx.metadata_general_license_link}" target="_blank">${caption.license}</a>`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return caption;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Transforms a GeoJSON feature from the STAC API into a PSV node.
|
|
290
|
+
*
|
|
291
|
+
* @param {object} f The API GeoJSON feature
|
|
292
|
+
* @param {object} t The labels translations container
|
|
293
|
+
* @param {boolean} [fastInternet] True if Internet speed is high enough for loading HD flat pictures
|
|
294
|
+
* @param {function} [customLinkFilter] A function checking if a STAC link is acceptable to use for picture navigation
|
|
295
|
+
* @return {object} A PSV node
|
|
296
|
+
* @private
|
|
297
|
+
*/
|
|
298
|
+
export function apiFeatureToPSVNode(f, t, fastInternet=false, customLinkFilter=null) {
|
|
299
|
+
const isHorizontalFovDefined = f.properties?.["pers:interior_orientation"]?.["field_of_view"] != null;
|
|
300
|
+
let horizontalFov = isHorizontalFovDefined ? parseInt(f.properties["pers:interior_orientation"]["field_of_view"]) : 70;
|
|
301
|
+
const is360 = horizontalFov === 360;
|
|
302
|
+
|
|
303
|
+
const hdUrl = (Object.values(f.assets).find(a => a?.roles?.includes("data")) || {}).href;
|
|
304
|
+
const matrix = f?.properties?.["tiles:tile_matrix_sets"]?.geovisio;
|
|
305
|
+
const prev = f.links?.find?.(l => l?.rel === "prev" && l?.type === "application/geo+json");
|
|
306
|
+
const next = f.links?.find?.(l => l?.rel === "next" && l?.type === "application/geo+json");
|
|
307
|
+
const baseUrlWebp = Object.values(f.assets).find(a => a.roles?.includes("visual") && a.type === "image/webp");
|
|
308
|
+
const baseUrlJpeg = Object.values(f.assets).find(a => a.roles?.includes("visual") && a.type === "image/jpeg");
|
|
309
|
+
const baseUrl = (baseUrlWebp || baseUrlJpeg).href;
|
|
310
|
+
const thumbUrl = (Object.values(f.assets).find(a => a.roles?.includes("thumbnail") && a.type === "image/jpeg"))?.href;
|
|
311
|
+
const tileUrl = f?.asset_templates?.tiles_webp || f?.asset_templates?.tiles;
|
|
312
|
+
const croppedPanoData = getCroppedPanoData(f);
|
|
313
|
+
const origInstance = f.links?.find?.(l => l?.rel === "via" && l?.type === "application/json");
|
|
314
|
+
|
|
315
|
+
let panorama;
|
|
316
|
+
|
|
317
|
+
// Cropped panorama
|
|
318
|
+
if(Object.keys(croppedPanoData).length > 0) {
|
|
319
|
+
panorama = {
|
|
320
|
+
baseUrl: fastInternet ? hdUrl : baseUrl,
|
|
321
|
+
origBaseUrl: fastInternet ? hdUrl : baseUrl,
|
|
322
|
+
hdUrl,
|
|
323
|
+
thumbUrl,
|
|
324
|
+
basePanoData: croppedPanoData,
|
|
325
|
+
// This is only to mock loading of tiles (which are not available for flat pictures)
|
|
326
|
+
cols: 2, rows: 1, width: 2, tileUrl: () => null
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
// 360°
|
|
330
|
+
else if(is360 && matrix) {
|
|
331
|
+
panorama = {
|
|
332
|
+
baseUrl,
|
|
333
|
+
origBaseUrl: baseUrl,
|
|
334
|
+
basePanoData: (img) => ({
|
|
335
|
+
fullWidth: img.width,
|
|
336
|
+
fullHeight: img.height,
|
|
337
|
+
}),
|
|
338
|
+
hdUrl,
|
|
339
|
+
thumbUrl,
|
|
340
|
+
cols: matrix && matrix.tileMatrix[0].matrixWidth,
|
|
341
|
+
rows: matrix && matrix.tileMatrix[0].matrixHeight,
|
|
342
|
+
width: matrix && (matrix.tileMatrix[0].matrixWidth * matrix.tileMatrix[0].tileWidth),
|
|
343
|
+
tileUrl: matrix && ((col, row) => tileUrl.href.replace(/\{TileCol\}/g, col).replace(/\{TileRow\}/g, row))
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// Flat pictures: shown only using a cropped base panorama
|
|
347
|
+
else {
|
|
348
|
+
panorama = {
|
|
349
|
+
baseUrl: fastInternet ? hdUrl : baseUrl,
|
|
350
|
+
origBaseUrl: fastInternet ? hdUrl : baseUrl,
|
|
351
|
+
hdUrl,
|
|
352
|
+
thumbUrl,
|
|
353
|
+
basePanoData: (img) => {
|
|
354
|
+
if (img.width < img.height && !isHorizontalFovDefined) {
|
|
355
|
+
horizontalFov = 35;
|
|
356
|
+
}
|
|
357
|
+
const verticalFov = horizontalFov * img.height / img.width;
|
|
358
|
+
const panoWidth = img.width * 360 / horizontalFov;
|
|
359
|
+
const panoHeight = img.height * 180 / verticalFov;
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
fullWidth: panoWidth,
|
|
363
|
+
fullHeight: panoHeight,
|
|
364
|
+
croppedWidth: img.width,
|
|
365
|
+
croppedHeight: img.height,
|
|
366
|
+
croppedX: (panoWidth - img.width) / 2,
|
|
367
|
+
croppedY: (panoHeight - img.height) / 2,
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
// This is only to mock loading of tiles (which are not available for flat pictures)
|
|
371
|
+
cols: 2, rows: 1, width: 2, tileUrl: () => null
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const node = {
|
|
376
|
+
id: f.id,
|
|
377
|
+
caption: getNodeCaption(f, t),
|
|
378
|
+
panorama,
|
|
379
|
+
links: filterRelatedPicsLinks(f, customLinkFilter),
|
|
380
|
+
gps: f.geometry.coordinates,
|
|
381
|
+
sequence: {
|
|
382
|
+
id: f.collection,
|
|
383
|
+
nextPic: next ? next.id : undefined,
|
|
384
|
+
prevPic: prev ? prev.id : undefined
|
|
385
|
+
},
|
|
386
|
+
sphereCorrection: getSphereCorrection(f),
|
|
387
|
+
horizontalFov,
|
|
388
|
+
origInstance,
|
|
389
|
+
properties: f.properties,
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
return node;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Filter surrounding pictures links to avoid too much arrows on viewer.
|
|
397
|
+
* @private
|
|
398
|
+
*/
|
|
399
|
+
export function filterRelatedPicsLinks(metadata, customFilter = null) {
|
|
400
|
+
const picLinks = metadata.links
|
|
401
|
+
.filter(l => ["next", "prev", "related"].includes(l?.rel) && l?.type === "application/geo+json")
|
|
402
|
+
.filter(l => customFilter ? customFilter(l) : true)
|
|
403
|
+
.map(l => {
|
|
404
|
+
if(l.datetime) {
|
|
405
|
+
l.date = l.datetime.split("T")[0];
|
|
406
|
+
}
|
|
407
|
+
return l;
|
|
408
|
+
});
|
|
409
|
+
const picPos = metadata.geometry.coordinates;
|
|
410
|
+
|
|
411
|
+
// Filter to keep a single link per direction, in same sequence or most recent one
|
|
412
|
+
const filteredLinks = [];
|
|
413
|
+
const picSurroundings = { "N": [], "ENE": [], "ESE": [], "S": [], "WSW": [], "WNW": [] };
|
|
414
|
+
|
|
415
|
+
for(let picLink of picLinks) {
|
|
416
|
+
const a = getSimplifiedAngle(picPos, picLink.geometry.coordinates);
|
|
417
|
+
picSurroundings[a].push(picLink);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
for(let direction in picSurroundings) {
|
|
421
|
+
const picsInDirection = picSurroundings[direction];
|
|
422
|
+
if(picsInDirection.length == 0) { continue; }
|
|
423
|
+
picsInDirection.sort(sortPicturesInDirection(picPos));
|
|
424
|
+
filteredLinks.push(picsInDirection.shift());
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let arrowStyle = l => l.rel === "related" ? {
|
|
428
|
+
element: getArrow(ArrowTurn),
|
|
429
|
+
size: { width: 64*2/3, height: 192*2/3 }
|
|
430
|
+
} : {
|
|
431
|
+
element: getArrow(ArrowTriangle),
|
|
432
|
+
size: { width: 75, height: 75 }
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const rectifiedYaw = - (metadata.properties?.["view:azimuth"] || 0) * (Math.PI / 180);
|
|
436
|
+
return filteredLinks.map(l => ({
|
|
437
|
+
nodeId: l.id,
|
|
438
|
+
gps: l.geometry.coordinates,
|
|
439
|
+
arrowStyle: arrowStyle(l),
|
|
440
|
+
linkOffset: { yaw: rectifiedYaw }
|
|
441
|
+
}));
|
|
442
|
+
}
|