@panoramax/web-viewer 5.0.0-develop-d26305dd → 5.0.0-develop-be5ba1a7
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/build/cjs/index.js +1 -1
- package/build/cjs/index_photoviewer.js +1 -1
- package/build/esm/components/core/Basic.js +1 -1
- package/build/esm/translations/el.json +92 -1
- package/package.json +1 -1
- package/build/bundle.cjs +0 -3399
- package/build/bundle.cjs.map +0 -1
- package/build/bundle_photoviewer.cjs +0 -2510
- package/build/bundle_photoviewer.cjs.map +0 -1
- package/build/components/core/Basic.css +0 -56
- package/build/components/core/Basic.js +0 -378
- package/build/components/core/CoverageMap.css +0 -10
- package/build/components/core/CoverageMap.js +0 -169
- package/build/components/core/Editor.css +0 -33
- package/build/components/core/Editor.js +0 -398
- package/build/components/core/PhotoViewer.css +0 -70
- package/build/components/core/PhotoViewer.js +0 -650
- package/build/components/core/Viewer.css +0 -130
- package/build/components/core/Viewer.js +0 -711
- package/build/components/core/index.js +0 -10
- package/build/components/index.js +0 -11
- package/build/components/index_photoviewer.js +0 -6
- package/build/components/layout/BottomDrawer.js +0 -258
- package/build/components/layout/CorneredGrid.js +0 -143
- package/build/components/layout/Mini.js +0 -121
- package/build/components/layout/Tabs.js +0 -140
- package/build/components/layout/index.js +0 -9
- package/build/components/menus/LocationPrecisionDoc.js +0 -42
- package/build/components/menus/MapBackground.js +0 -110
- package/build/components/menus/MapFilters.js +0 -567
- package/build/components/menus/MapLayers.js +0 -238
- package/build/components/menus/MapLegend.js +0 -68
- package/build/components/menus/MiniPictureLegend.js +0 -73
- package/build/components/menus/PictureLegend.js +0 -379
- package/build/components/menus/PictureMetadata.js +0 -380
- package/build/components/menus/PlayerOptions.js +0 -93
- package/build/components/menus/QualityScoreDoc.js +0 -42
- package/build/components/menus/ReportForm.js +0 -132
- package/build/components/menus/SemanticsDoc.js +0 -38
- package/build/components/menus/SemanticsDownload.js +0 -33
- package/build/components/menus/SemanticsFilters.js +0 -153
- package/build/components/menus/SemanticsList.js +0 -413
- package/build/components/menus/SemanticsMetadata.js +0 -368
- package/build/components/menus/Share.js +0 -105
- package/build/components/menus/index.js +0 -22
- package/build/components/menus/index_photoviewer.js +0 -11
- package/build/components/styles.js +0 -557
- package/build/components/ui/AnnotationsSwitch.js +0 -159
- package/build/components/ui/Button.js +0 -77
- package/build/components/ui/ButtonGroup.css +0 -59
- package/build/components/ui/ButtonGroup.js +0 -69
- package/build/components/ui/CopyButton.js +0 -110
- package/build/components/ui/Grade.js +0 -54
- package/build/components/ui/GradeFilter.js +0 -122
- package/build/components/ui/IconSwitch.js +0 -193
- package/build/components/ui/LinkButton.js +0 -67
- package/build/components/ui/ListGroup.js +0 -66
- package/build/components/ui/ListItem.js +0 -90
- package/build/components/ui/Loader.js +0 -203
- package/build/components/ui/Map.css +0 -63
- package/build/components/ui/Map.js +0 -853
- package/build/components/ui/MapMore.js +0 -175
- package/build/components/ui/Photo.css +0 -50
- package/build/components/ui/Photo.js +0 -1502
- package/build/components/ui/Popup.js +0 -145
- package/build/components/ui/ProgressBar.js +0 -104
- package/build/components/ui/QualityScore.js +0 -147
- package/build/components/ui/SearchBar.js +0 -374
- package/build/components/ui/SemanticsEditor.js +0 -191
- package/build/components/ui/SemanticsTable.js +0 -88
- package/build/components/ui/Switch.js +0 -139
- package/build/components/ui/TogglableGroup.js +0 -157
- package/build/components/ui/index.js +0 -29
- package/build/components/ui/index_photoviewer.js +0 -21
- package/build/components/ui/widgets/CopyCoordinates.js +0 -75
- package/build/components/ui/widgets/GeoSearch.css +0 -21
- package/build/components/ui/widgets/GeoSearch.js +0 -150
- package/build/components/ui/widgets/Legend.js +0 -190
- package/build/components/ui/widgets/LevelSelect.css +0 -51
- package/build/components/ui/widgets/LevelSelect.js +0 -143
- package/build/components/ui/widgets/MapFiltersButton.js +0 -114
- package/build/components/ui/widgets/MapLayersButton.js +0 -79
- package/build/components/ui/widgets/OSMEditors.js +0 -155
- package/build/components/ui/widgets/PictureLegendActions.js +0 -99
- package/build/components/ui/widgets/Player.css +0 -7
- package/build/components/ui/widgets/Player.js +0 -154
- package/build/components/ui/widgets/SemanticsFiltersButton.js +0 -65
- package/build/components/ui/widgets/Zoom.js +0 -84
- package/build/components/ui/widgets/index.js +0 -16
- package/build/components/ui/widgets/index_photoviewer.js +0 -7
- package/build/img/arrow_360.svg +0 -14
- package/build/img/arrow_flat.svg +0 -11
- package/build/img/arrow_triangle.svg +0 -9
- package/build/img/arrow_turn.svg +0 -8
- package/build/img/bg_aerial.jpg +0 -0
- package/build/img/bg_streets.jpg +0 -0
- package/build/img/loader_base.jpg +0 -0
- package/build/img/logo_dead.svg +0 -91
- package/build/img/marker.svg +0 -17
- package/build/img/marker_blue.svg +0 -20
- package/build/img/osm.svg +0 -49
- package/build/img/panoramax.svg +0 -13
- package/build/img/switch_big.svg +0 -54
- package/build/img/switch_mini.svg +0 -48
- package/build/img/wd.svg +0 -1
- package/build/index_photoviewer.js +0 -4
- package/build/package.json +0 -148
- package/build/servers.js +0 -14
- package/build/translations/ar.json +0 -1
- package/build/translations/be.json +0 -257
- package/build/translations/br.json +0 -81
- package/build/translations/cy.json +0 -117
- package/build/translations/da.json +0 -300
- package/build/translations/de.json +0 -309
- package/build/translations/en.json +0 -294
- package/build/translations/eo.json +0 -235
- package/build/translations/es.json +0 -292
- package/build/translations/fi.json +0 -1
- package/build/translations/fr.json +0 -294
- package/build/translations/hr.json +0 -294
- package/build/translations/hu.json +0 -294
- package/build/translations/it.json +0 -306
- package/build/translations/ja.json +0 -182
- package/build/translations/ko.json +0 -1
- package/build/translations/nl.json +0 -305
- package/build/translations/nn.json +0 -1
- package/build/translations/pl.json +0 -169
- package/build/translations/pt.json +0 -296
- package/build/translations/pt_BR.json +0 -304
- package/build/translations/sv.json +0 -182
- package/build/translations/ti.json +0 -9
- package/build/translations/tr.json +0 -297
- package/build/translations/uk.json +0 -268
- package/build/translations/zh_Hant.json +0 -309
- package/build/utils/API.js +0 -928
- package/build/utils/InitParameters.js +0 -521
- package/build/utils/MapStyleComposer.js +0 -889
- package/build/utils/PanoraMapProtocol.js +0 -49
- package/build/utils/PhotoAdapter.js +0 -49
- package/build/utils/PresetsManager.js +0 -148
- package/build/utils/SemanticsMapProtocol.js +0 -144
- package/build/utils/URLHandler.js +0 -426
- package/build/utils/geocoder.js +0 -203
- package/build/utils/i18n.js +0 -128
- package/build/utils/index.js +0 -17
- package/build/utils/index_photoviewer.js +0 -14
- package/build/utils/indoor.js +0 -200
- package/build/utils/map.js +0 -788
- package/build/utils/picture.js +0 -507
- package/build/utils/semantics.js +0 -321
- package/build/utils/services.js +0 -148
- package/build/utils/utils.js +0 -433
- package/build/utils/widgets.js +0 -110
package/build/utils/picture.js
DELETED
|
@@ -1,507 +0,0 @@
|
|
|
1
|
-
import { COLORS, getArrow, getDistance, getSimplifiedAngle, svgToPSVLink } from "./utils.js";
|
|
2
|
-
const ArrowTriangleSVG = await fetch(new URL("../img/arrow_triangle.svg", import.meta.url).href).then(res => res.text());
|
|
3
|
-
const ArrowTurnSVG = await fetch(new URL("../img/arrow_turn.svg", import.meta.url).href).then(res => res.text());
|
|
4
|
-
const ArrowTriangle = svgToPSVLink(ArrowTriangleSVG, "white");
|
|
5
|
-
const ArrowTurn = svgToPSVLink(ArrowTurnSVG, COLORS.NEXT);
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Read float value from EXIF tags (to handle fractions & all)
|
|
9
|
-
* @param {*} val The input EXIF tag value
|
|
10
|
-
* @returns {number|undefined} The parsed value, or undefined if value is not readable
|
|
11
|
-
* @private
|
|
12
|
-
*/
|
|
13
|
-
export function getExifFloat(val) {
|
|
14
|
-
// Null-like values
|
|
15
|
-
if(
|
|
16
|
-
[null, undefined, ""].includes(val)
|
|
17
|
-
|| typeof val === "string" && val.trim() === ""
|
|
18
|
-
) {
|
|
19
|
-
return undefined;
|
|
20
|
-
}
|
|
21
|
-
// Already valid number
|
|
22
|
-
else if(typeof val === "number") {
|
|
23
|
-
return val;
|
|
24
|
-
}
|
|
25
|
-
// String
|
|
26
|
-
else if(typeof val === "string") {
|
|
27
|
-
// Check if looks like a fraction
|
|
28
|
-
if(/^-?\d+(\.\d+)?\/-?\d+(\.\d+)?$/.test(val)) {
|
|
29
|
-
const parts = val.split("/").map(p => parseFloat(p));
|
|
30
|
-
return parts[0] / parts[1];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Try a direct cast to float
|
|
34
|
-
try { return parseFloat(val); }
|
|
35
|
-
|
|
36
|
-
catch(e) {}
|
|
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
|
-
* @param {object} [img] The loaded image file, with width, height properties for possible resizing
|
|
147
|
-
* @returns {object} The PSV panoData values
|
|
148
|
-
* @private
|
|
149
|
-
*/
|
|
150
|
-
export function getCroppedPanoData(picture, img) {
|
|
151
|
-
let res;
|
|
152
|
-
|
|
153
|
-
if(picture.properties?.["pers:interior_orientation"]) {
|
|
154
|
-
if(
|
|
155
|
-
picture.properties["pers:interior_orientation"]?.["visible_area"]
|
|
156
|
-
&& picture.properties["pers:interior_orientation"]?.["sensor_array_dimensions"]
|
|
157
|
-
) {
|
|
158
|
-
const va = picture.properties["pers:interior_orientation"]["visible_area"];
|
|
159
|
-
const sad = picture.properties["pers:interior_orientation"]["sensor_array_dimensions"];
|
|
160
|
-
try {
|
|
161
|
-
res = {
|
|
162
|
-
fullWidth: parseInt(sad[0]),
|
|
163
|
-
fullHeight: parseInt(sad[1]),
|
|
164
|
-
croppedX: parseInt(va[0]),
|
|
165
|
-
croppedY: parseInt(va[1]),
|
|
166
|
-
croppedWidth: parseInt(sad[0]) - parseInt(va[2]) - parseInt(va[0]),
|
|
167
|
-
croppedHeight: parseInt(sad[1]) - parseInt(va[3]) - parseInt(va[1]),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
catch(e) {
|
|
172
|
-
console.warn("Invalid pers:interior_orientation values for cropped panorama "+picture.id);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if(!res && picture.properties?.exif) {
|
|
178
|
-
try {
|
|
179
|
-
res = {
|
|
180
|
-
fullWidth: parseInt(picture.properties.exif?.["Xmp.GPano.FullPanoWidthPixels"]),
|
|
181
|
-
fullHeight: parseInt(picture.properties.exif?.["Xmp.GPano.FullPanoHeightPixels"]),
|
|
182
|
-
croppedX: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaLeftPixels"]),
|
|
183
|
-
croppedY: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaTopPixels"]),
|
|
184
|
-
croppedWidth: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaImageWidthPixels"]),
|
|
185
|
-
croppedHeight: parseInt(picture.properties.exif?.["Xmp.GPano.CroppedAreaImageHeightPixels"]),
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// Fix images with cropped size higher than full size
|
|
189
|
-
if(res.croppedWidth > res.fullWidth) { res.fullWidth = res.croppedWidth; }
|
|
190
|
-
if(res.croppedHeight > res.fullHeight) { res.fullHeight = res.croppedHeight; }
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
catch(e) {
|
|
194
|
-
console.warn("Invalid XMP.GPano values for cropped panorama "+picture.id);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Check if crop is really necessary
|
|
199
|
-
if(res) {
|
|
200
|
-
res = Object.fromEntries(Object.entries(res || {}).filter(e => !isNaN(e[1])));
|
|
201
|
-
if(
|
|
202
|
-
(!res.fullWidth && !res.croppedWidth && res.fullHeight && !res.croppedHeight)
|
|
203
|
-
|| (res.fullWidth && !res.croppedWidth && !res.fullHeight && !res.croppedHeight)
|
|
204
|
-
|| (res.fullWidth && !res.croppedWidth && res.fullHeight && !res.croppedHeight)
|
|
205
|
-
// eslint-disable-next-line eqeqeq
|
|
206
|
-
|| (res.fullWidth == res.croppedWidth && res.fullHeight == res.croppedHeight)
|
|
207
|
-
) {
|
|
208
|
-
res = {};
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Resize if image given
|
|
213
|
-
if(res?.fullWidth && img?.width) {
|
|
214
|
-
let ratio = img.width / (res?.croppedWidth || res.fullWidth);
|
|
215
|
-
res = {
|
|
216
|
-
fullWidth: res.fullWidth !== undefined ? Math.floor(res.fullWidth * ratio) : undefined,
|
|
217
|
-
fullHeight: res.fullHeight !== undefined ? Math.floor(res.fullHeight * ratio) : undefined,
|
|
218
|
-
croppedWidth: res.croppedWidth !== undefined ? Math.floor(res.croppedWidth * ratio) : undefined,
|
|
219
|
-
croppedHeight: res.croppedHeight !== undefined ? Math.floor(res.croppedHeight * ratio) : undefined,
|
|
220
|
-
croppedX: res.croppedX !== undefined ? Math.floor(res.croppedX * ratio) : undefined,
|
|
221
|
-
croppedY: res.croppedY !== undefined ? Math.floor(res.croppedY * ratio) : undefined,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return res || {};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Compare function to retrieve most appropriate picture in a single direction.
|
|
230
|
-
*
|
|
231
|
-
* @param {number[]} picPos The picture [x,y] position
|
|
232
|
-
* @returns {function} A compare function for sorting
|
|
233
|
-
* @private
|
|
234
|
-
*/
|
|
235
|
-
export function sortPicturesInDirection(picPos) {
|
|
236
|
-
return (a,b) => {
|
|
237
|
-
// Two prev/next links = no sort
|
|
238
|
-
if(a.rel !== "related" && b.rel !== "related") { return 0; }
|
|
239
|
-
// First is prev/next link = goes first
|
|
240
|
-
else if(a.rel !== "related") { return -1; }
|
|
241
|
-
// Second is prev/next link = goes first
|
|
242
|
-
else if(b.rel !== "related") { return 1; }
|
|
243
|
-
// Two related links same day = nearest goes first
|
|
244
|
-
else if(a.date === b.date) { return getDistance(picPos, a.geometry.coordinates) - getDistance(picPos, b.geometry.coordinates); }
|
|
245
|
-
// Two related links at different day = recent goes first
|
|
246
|
-
else { return b.date.localeCompare(a.date); }
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Generates the navbar caption based on a single picture metadata
|
|
252
|
-
*
|
|
253
|
-
* @param {object} metadata The picture metadata
|
|
254
|
-
* @param {object} t The labels translations container
|
|
255
|
-
* @returns {object} Normalized object with user name, licence and date
|
|
256
|
-
* @private
|
|
257
|
-
*/
|
|
258
|
-
export function getNodeCaption(metadata, t) {
|
|
259
|
-
const caption = {};
|
|
260
|
-
|
|
261
|
-
// Timestamp
|
|
262
|
-
if(metadata?.properties?.datetimetz) {
|
|
263
|
-
caption.date = new Date(metadata.properties.datetimetz);
|
|
264
|
-
const timeZoneMatch = metadata.properties.datetimetz.match(/([+-]\d{2}):(\d{2})$|Z$/);
|
|
265
|
-
if (timeZoneMatch) {
|
|
266
|
-
if (timeZoneMatch[0] === "Z") {
|
|
267
|
-
caption.tz = "UTC";
|
|
268
|
-
} else {
|
|
269
|
-
caption.tz = timeZoneMatch[0];
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
else if(metadata?.properties?.datetime) {
|
|
274
|
-
caption.date = new Date(metadata.properties.datetime);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Producer
|
|
278
|
-
if(metadata?.providers) {
|
|
279
|
-
const producerRoles = metadata?.providers?.filter(el => el?.roles?.includes("producer"));
|
|
280
|
-
if(producerRoles?.length >= 0) {
|
|
281
|
-
// Avoid duplicates between account name and picture author
|
|
282
|
-
const producersDeduped = {};
|
|
283
|
-
producerRoles.map(p => p.name).forEach(p => {
|
|
284
|
-
const pmin = p.toLowerCase().replace(/\s/g, "");
|
|
285
|
-
if(producersDeduped[pmin]) { producersDeduped[pmin].push(p); }
|
|
286
|
-
else { producersDeduped[pmin] = [p];}
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// Keep best looking name for each
|
|
290
|
-
caption.producer = [];
|
|
291
|
-
Object.values(producersDeduped).forEach(pv => {
|
|
292
|
-
const deflt = pv[0];
|
|
293
|
-
const better = pv.find(v => v.toLowerCase() !== v);
|
|
294
|
-
caption.producer.push(better || deflt);
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// License
|
|
300
|
-
if(metadata?.properties?.license) {
|
|
301
|
-
caption.license = metadata.properties.license;
|
|
302
|
-
// Look for URL to license
|
|
303
|
-
if(metadata?.links) {
|
|
304
|
-
const licenseLink = metadata.links.find(l => l?.rel === "license");
|
|
305
|
-
if(licenseLink) {
|
|
306
|
-
caption.license = `<a href="${licenseLink.href}" title="${t.pnx.metadata_general_license_link}" target="_blank">${caption.license}</a>`;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return caption;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Transforms a GeoJSON feature from the STAC API into a PSV node.
|
|
316
|
-
*
|
|
317
|
-
* @param {object} f The API GeoJSON feature
|
|
318
|
-
* @param {object} t The labels translations container
|
|
319
|
-
* @param {boolean} [fastInternet] True if Internet speed is high enough for loading HD flat pictures
|
|
320
|
-
* @param {function} [customLinkFilter] A function checking if a STAC link is acceptable to use for picture navigation
|
|
321
|
-
* @param {function} [urlCleaner] A function that each URL goes through for cleaning (allows use of relative URLs)
|
|
322
|
-
* @return {object} A PSV node
|
|
323
|
-
* @private
|
|
324
|
-
*/
|
|
325
|
-
export function apiFeatureToPSVNode(f, t, fastInternet=false, customLinkFilter=null, urlCleaner=(u => u)) {
|
|
326
|
-
// eslint-disable-next-line eqeqeq
|
|
327
|
-
const isHorizontalFovDefined = f.properties?.["pers:interior_orientation"]?.["field_of_view"] != null;
|
|
328
|
-
let horizontalFov = isHorizontalFovDefined ? parseInt(f.properties["pers:interior_orientation"]["field_of_view"]) : 70;
|
|
329
|
-
const is360 = horizontalFov === 360;
|
|
330
|
-
|
|
331
|
-
const hdUrl = urlCleaner((Object.values(f.assets).find(a => a?.roles?.includes("data")) || {}).href);
|
|
332
|
-
const matrix = f?.properties?.["tiles:tile_matrix_sets"]?.geovisio;
|
|
333
|
-
const prev = f.links?.find?.(l => l?.rel === "prev" && l?.type === "application/geo+json");
|
|
334
|
-
const next = f.links?.find?.(l => l?.rel === "next" && l?.type === "application/geo+json");
|
|
335
|
-
const baseUrlWebp = Object.values(f.assets).find(a => a.roles?.includes("visual") && a.type === "image/webp");
|
|
336
|
-
const baseUrlJpeg = Object.values(f.assets).find(a => a.roles?.includes("visual") && a.type === "image/jpeg");
|
|
337
|
-
const baseUrl = urlCleaner((baseUrlWebp || baseUrlJpeg).href);
|
|
338
|
-
const thumbUrl = urlCleaner((Object.values(f.assets).find(a => a.roles?.includes("thumbnail") && a.type === "image/jpeg"))?.href);
|
|
339
|
-
const tileUrl = f?.asset_templates?.tiles_webp || f?.asset_templates?.tiles;
|
|
340
|
-
const croppedPanoData = getCroppedPanoData(f);
|
|
341
|
-
const origInstance = f.links?.find?.(l => l?.rel === "via" && l?.type === "application/json");
|
|
342
|
-
const size = f.properties?.["pers:interior_orientation"]?.["sensor_array_dimensions"];
|
|
343
|
-
|
|
344
|
-
let panorama;
|
|
345
|
-
|
|
346
|
-
// Cropped panorama
|
|
347
|
-
if(Object.keys(croppedPanoData).length > 0) {
|
|
348
|
-
panorama = {
|
|
349
|
-
baseUrl: fastInternet ? hdUrl : baseUrl,
|
|
350
|
-
origBaseUrl: fastInternet ? hdUrl : baseUrl,
|
|
351
|
-
hdUrl,
|
|
352
|
-
thumbUrl,
|
|
353
|
-
basePanoData: fastInternet ? croppedPanoData : (img) => getCroppedPanoData(f, img),
|
|
354
|
-
cropData: croppedPanoData,
|
|
355
|
-
width: croppedPanoData.fullWidth,
|
|
356
|
-
// This is only to mock loading of tiles (which are not available for flat pictures)
|
|
357
|
-
cols: 2, rows: 1, tileUrl: () => null
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
// 360°
|
|
361
|
-
else if(is360 && matrix) {
|
|
362
|
-
panorama = {
|
|
363
|
-
baseUrl,
|
|
364
|
-
origBaseUrl: baseUrl,
|
|
365
|
-
basePanoData: (img) => ({
|
|
366
|
-
fullWidth: img.width,
|
|
367
|
-
fullHeight: img.height,
|
|
368
|
-
}),
|
|
369
|
-
hdUrl,
|
|
370
|
-
thumbUrl,
|
|
371
|
-
cols: matrix && matrix.tileMatrix[0].matrixWidth,
|
|
372
|
-
rows: matrix && matrix.tileMatrix[0].matrixHeight,
|
|
373
|
-
width: matrix && (matrix.tileMatrix[0].matrixWidth * matrix.tileMatrix[0].tileWidth),
|
|
374
|
-
tileUrl: matrix && ((col, row) => urlCleaner(tileUrl.href.replace(/\{TileCol\}/g, col).replace(/\{TileRow\}/g, row)))
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
// Flat pictures: shown only using a cropped base panorama
|
|
378
|
-
else {
|
|
379
|
-
const getFlatCrop = (img) => {
|
|
380
|
-
if (img.width < img.height && !isHorizontalFovDefined) {
|
|
381
|
-
horizontalFov = 35;
|
|
382
|
-
}
|
|
383
|
-
const verticalFov = horizontalFov * img.height / img.width;
|
|
384
|
-
const panoWidth = img.width * 360 / horizontalFov;
|
|
385
|
-
const panoHeight = img.height * 180 / verticalFov;
|
|
386
|
-
|
|
387
|
-
return {
|
|
388
|
-
fullWidth: Math.floor(panoWidth),
|
|
389
|
-
fullHeight: Math.floor(panoHeight),
|
|
390
|
-
croppedWidth: img.width,
|
|
391
|
-
croppedHeight: img.height,
|
|
392
|
-
croppedX: Math.floor((panoWidth - img.width) / 2),
|
|
393
|
-
croppedY: Math.floor((panoHeight - img.height) / 2),
|
|
394
|
-
};
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
let cropData = size ? getFlatCrop({width: size[0], height: size[1]}) : null;
|
|
398
|
-
|
|
399
|
-
panorama = {
|
|
400
|
-
baseUrl: fastInternet ? hdUrl : baseUrl,
|
|
401
|
-
origBaseUrl: fastInternet ? hdUrl : baseUrl,
|
|
402
|
-
hdUrl,
|
|
403
|
-
thumbUrl,
|
|
404
|
-
basePanoData: getFlatCrop,
|
|
405
|
-
cropData,
|
|
406
|
-
width: cropData?.fullWidth || 2,
|
|
407
|
-
// This is only to mock loading of tiles (which are not available for flat pictures)
|
|
408
|
-
cols: 2, rows: 1, tileUrl: () => null
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Cleanup empty semantics feature (metacatalog bug)
|
|
413
|
-
if(f.properties?.semantics) {
|
|
414
|
-
f.properties.semantics = f.properties.semantics.filter(o => Object.keys(o).length > 0);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const node = {
|
|
418
|
-
id: f.id,
|
|
419
|
-
caption: getNodeCaption(f, t),
|
|
420
|
-
panorama,
|
|
421
|
-
links: filterRelatedPicsLinks(f, customLinkFilter),
|
|
422
|
-
gps: f.geometry.coordinates,
|
|
423
|
-
sequence: {
|
|
424
|
-
id: f.collection,
|
|
425
|
-
nextPic: next ? next.id : undefined,
|
|
426
|
-
prevPic: prev ? prev.id : undefined
|
|
427
|
-
},
|
|
428
|
-
sphereCorrection: getSphereCorrection(f),
|
|
429
|
-
horizontalFov,
|
|
430
|
-
origInstance,
|
|
431
|
-
properties: f.properties,
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
return node;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Filter surrounding pictures links to avoid too much arrows on viewer.
|
|
439
|
-
* @private
|
|
440
|
-
*/
|
|
441
|
-
export function filterRelatedPicsLinks(metadata, customFilter = null) {
|
|
442
|
-
const picLinks = metadata.links
|
|
443
|
-
.filter(l => ["next", "prev", "related"].includes(l?.rel) && l?.type === "application/geo+json")
|
|
444
|
-
.filter(l => customFilter ? customFilter(l) : true)
|
|
445
|
-
.map(l => {
|
|
446
|
-
if(l.datetime) {
|
|
447
|
-
l.date = l.datetime.split("T")[0];
|
|
448
|
-
}
|
|
449
|
-
return l;
|
|
450
|
-
});
|
|
451
|
-
const picPos = metadata.geometry.coordinates;
|
|
452
|
-
|
|
453
|
-
// Filter to keep a single link per direction, in same sequence or most recent one
|
|
454
|
-
const filteredLinks = [];
|
|
455
|
-
const picSurroundings = { "N": [], "ENE": [], "ESE": [], "S": [], "WSW": [], "WNW": [] };
|
|
456
|
-
|
|
457
|
-
for(let picLink of picLinks) {
|
|
458
|
-
const a = getSimplifiedAngle(picPos, picLink.geometry.coordinates);
|
|
459
|
-
picSurroundings[a].push(picLink);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
for(let direction in picSurroundings) {
|
|
463
|
-
const picsInDirection = picSurroundings[direction];
|
|
464
|
-
if(picsInDirection.length === 0) { continue; }
|
|
465
|
-
picsInDirection.sort(sortPicturesInDirection(picPos));
|
|
466
|
-
filteredLinks.push(picsInDirection.shift());
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
let arrowStyle = l => l.rel === "related" ? {
|
|
470
|
-
element: getArrow(ArrowTurn),
|
|
471
|
-
size: { width: 64*2/3, height: 192*2/3 }
|
|
472
|
-
} : {
|
|
473
|
-
element: getArrow(ArrowTriangle),
|
|
474
|
-
size: { width: 75, height: 75 }
|
|
475
|
-
};
|
|
476
|
-
|
|
477
|
-
const rectifiedYaw = - (metadata.properties?.["view:azimuth"] || 0) * (Math.PI / 180);
|
|
478
|
-
return filteredLinks.map(l => ({
|
|
479
|
-
nodeId: l.id,
|
|
480
|
-
gps: l.geometry.coordinates,
|
|
481
|
-
arrowStyle: arrowStyle(l),
|
|
482
|
-
linkOffset: { yaw: rectifiedYaw }
|
|
483
|
-
}));
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Read structured hashtags from picture metadata
|
|
488
|
-
* @private
|
|
489
|
-
*/
|
|
490
|
-
export function getHashTags(metadata) {
|
|
491
|
-
if(!metadata?.properties?.semantics) { return []; }
|
|
492
|
-
|
|
493
|
-
// Find hashtag entry in semantics
|
|
494
|
-
const hashtagsTags = metadata.properties.semantics.filter(kv => kv.key === "hashtags");
|
|
495
|
-
const readHashTags = [];
|
|
496
|
-
hashtagsTags.forEach(htt => htt.value.split(";").forEach(v => readHashTags.push(v.trim())));
|
|
497
|
-
|
|
498
|
-
return readHashTags;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Check if a given picture has associated annotations.
|
|
503
|
-
* @private
|
|
504
|
-
*/
|
|
505
|
-
export function hasAnnotations(metadata) {
|
|
506
|
-
return metadata?.properties?.annotations?.length > 0;
|
|
507
|
-
}
|