forked from mistic100/Photo-Sphere-Viewer
-
Notifications
You must be signed in to change notification settings - Fork 1
/
psv.js
371 lines (325 loc) · 9.24 KB
/
psv.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
import { LinearFilter, MathUtils, Quaternion, Texture } from 'three';
import { PSVError } from '../PSVError';
import { loop } from './math';
/**
* @summary Returns the plugin constructor from the imported object
* For retrocompatibility with previous default exports
* @memberOf PSV.utils
* @package
*/
export function pluginInterop(plugin, target) {
if (plugin) {
for (const [, p] of [['_', plugin], ...Object.entries(plugin)]) {
if (p.prototype instanceof target) {
return p;
}
}
}
return null;
}
/**
* @summary Builds an Error with name 'AbortError'
* @memberOf PSV.utils
* @return {Error}
*/
export function getAbortError() {
const error = new Error('Loading was aborted.');
error.name = 'AbortError';
return error;
}
/**
* @summary Tests if an Error has name 'AbortError'
* @memberOf PSV.utils
* @param {Error} err
* @return {boolean}
*/
export function isAbortError(err) {
return err?.name === 'AbortError';
}
/**
* @summary Displays a warning in the console
* @memberOf PSV.utils
* @param {string} message
*/
export function logWarn(message) {
console.warn(`PhotoSphereViewer: ${message}`);
}
/**
* @summary Checks if an object is a {PSV.ExtendedPosition}, ie has x/y or longitude/latitude
* @memberOf PSV.utils
* @param {object} object
* @returns {boolean}
*/
export function isExtendedPosition(object) {
return [['x', 'y'], ['longitude', 'latitude']].some(([key1, key2]) => {
return object[key1] !== undefined && object[key2] !== undefined;
});
}
/**
* @summary Returns the value of a given attribute in the panorama metadata
* @memberOf PSV.utils
* @param {string} data
* @param {string} attr
* @returns (number)
*/
export function getXMPValue(data, attr) {
// XMP data are stored in children
let result = data.match('<GPano:' + attr + '>(.*)</GPano:' + attr + '>');
if (result !== null) {
const val = parseInt(result[1], 10);
return isNaN(val) ? null : val;
}
// XMP data are stored in attributes
result = data.match('GPano:' + attr + '="(.*?)"');
if (result !== null) {
const val = parseInt(result[1], 10);
return isNaN(val) ? null : val;
}
return null;
}
/**
* @readonly
* @private
* @type {{top: string, left: string, bottom: string, center: string, right: string}}
*/
const CSS_POSITIONS = {
top : '0%',
bottom: '100%',
left : '0%',
right : '100%',
center: '50%',
};
/**
* @summary Translate CSS values like "top center" or "10% 50%" as top and left positions
* @memberOf PSV.utils
* @description The implementation is as close as possible to the "background-position" specification
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-position}
* @param {string|PSV.Point} value
* @returns {PSV.Point}
*/
export function parsePosition(value) {
if (!value) {
return { x: 0.5, y: 0.5 };
}
if (typeof value === 'object') {
return value;
}
let tokens = value.toLocaleLowerCase().split(' ').slice(0, 2);
if (tokens.length === 1) {
if (CSS_POSITIONS[tokens[0]] !== undefined) {
tokens = [tokens[0], 'center'];
}
else {
tokens = [tokens[0], tokens[0]];
}
}
const xFirst = tokens[1] !== 'left' && tokens[1] !== 'right' && tokens[0] !== 'top' && tokens[0] !== 'bottom';
tokens = tokens.map(token => CSS_POSITIONS[token] || token);
if (!xFirst) {
tokens.reverse();
}
const parsed = tokens.join(' ').match(/^([0-9.]+)% ([0-9.]+)%$/);
if (parsed) {
return {
x: parseFloat(parsed[1]) / 100,
y: parseFloat(parsed[2]) / 100,
};
}
else {
return { x: 0.5, y: 0.5 };
}
}
/**
* @readonly
* @private
*/
const X_VALUES = ['left', 'center', 'right'];
/**
* @readonly
* @private
*/
const Y_VALUES = ['top', 'center', 'bottom'];
/**
* @readonly
* @private
*/
const POS_VALUES = [...X_VALUES, ...Y_VALUES];
/**
* @readonly
* @private
*/
const CENTER = 'center';
/**
* @summary Parse a CSS-like position into an array of position keywords among top, bottom, left, right and center
* @memberOf PSV.utils
* @param {string | string[]} value
* @param {object} [options]
* @param {boolean} [options.allowCenter=true] allow "center center"
* @param {boolean} [options.cssOrder=true] force CSS order (y axis then x axis)
* @return {string[]}
*/
export function cleanPosition(value, { allowCenter, cssOrder } = { allowCenter: true, cssOrder: true }) {
if (!value) {
return null;
}
if (typeof value === 'string') {
value = value.split(' ');
}
if (value.length === 1) {
if (value[0] === CENTER) {
value = [CENTER, CENTER];
}
else if (X_VALUES.indexOf(value[0]) !== -1) {
value = [CENTER, value[0]];
}
else if (Y_VALUES.indexOf(value[0]) !== -1) {
value = [value[0], CENTER];
}
}
if (value.length !== 2 || POS_VALUES.indexOf(value[0]) === -1 || POS_VALUES.indexOf(value[1]) === -1) {
logWarn(`Unparsable position ${value}`);
return null;
}
if (!allowCenter && value[0] === CENTER && value[1] === CENTER) {
logWarn(`Invalid position center center`);
return null;
}
if (cssOrder && !positionIsOrdered(value)) {
value = [value[1], value[0]];
}
if (value[1] === CENTER && X_VALUES.indexOf(value[0]) !== -1) {
value = [CENTER, value[0]];
}
if (value[0] === CENTER && Y_VALUES.indexOf(value[1]) !== -1) {
value = [value[1], CENTER];
}
return value;
}
/**
* @summary Checks if an array of two positions is ordered (y axis then x axis)
* @param {string[]} value
* @return {boolean}
*/
export function positionIsOrdered(value) {
return Y_VALUES.indexOf(value[0]) !== -1 && X_VALUES.indexOf(value[1]) !== -1;
}
/**
* @summary Parses an speed
* @memberOf PSV.utils
* @param {string|number} speed - The speed, in radians/degrees/revolutions per second/minute
* @returns {number} radians per second
* @throws {PSV.PSVError} when the speed cannot be parsed
*/
export function parseSpeed(speed) {
let parsed;
if (typeof speed === 'string') {
const speedStr = speed.toString().trim();
// Speed extraction
let speedValue = parseFloat(speedStr.replace(/^(-?[0-9]+(?:\.[0-9]*)?).*$/, '$1'));
const speedUnit = speedStr.replace(/^-?[0-9]+(?:\.[0-9]*)?(.*)$/, '$1').trim();
// "per minute" -> "per second"
if (speedUnit.match(/(pm|per minute)$/)) {
speedValue /= 60;
}
// Which unit?
switch (speedUnit) {
// Degrees per minute / second
case 'dpm':
case 'degrees per minute':
case 'dps':
case 'degrees per second':
parsed = MathUtils.degToRad(speedValue);
break;
// Radians per minute / second
case 'rdpm':
case 'radians per minute':
case 'rdps':
case 'radians per second':
parsed = speedValue;
break;
// Revolutions per minute / second
case 'rpm':
case 'revolutions per minute':
case 'rps':
case 'revolutions per second':
parsed = speedValue * Math.PI * 2;
break;
// Unknown unit
default:
throw new PSVError('Unknown speed unit "' + speedUnit + '"');
}
}
else {
parsed = speed;
}
return parsed;
}
/**
* @summary Parses an angle value in radians or degrees and returns a normalized value in radians
* @memberOf PSV.utils
* @param {string|number} angle - eg: 3.14, 3.14rad, 180deg
* @param {boolean} [zeroCenter=false] - normalize between -Pi - Pi instead of 0 - 2*Pi
* @param {boolean} [halfCircle=zeroCenter] - normalize between -Pi/2 - Pi/2 instead of -Pi - Pi
* @returns {number}
* @throws {PSV.PSVError} when the angle cannot be parsed
*/
export function parseAngle(angle, zeroCenter = false, halfCircle = zeroCenter) {
let parsed;
if (typeof angle === 'string') {
const match = angle.toLowerCase().trim().match(/^(-?[0-9]+(?:\.[0-9]*)?)(.*)$/);
if (!match) {
throw new PSVError('Unknown angle "' + angle + '"');
}
const value = parseFloat(match[1]);
const unit = match[2];
if (unit) {
switch (unit) {
case 'deg':
case 'degs':
parsed = MathUtils.degToRad(value);
break;
case 'rad':
case 'rads':
parsed = value;
break;
default:
throw new PSVError('Unknown angle unit "' + unit + '"');
}
}
else {
parsed = value;
}
}
else if (typeof angle === 'number' && !isNaN(angle)) {
parsed = angle;
}
else {
throw new PSVError('Unknown angle "' + angle + '"');
}
parsed = loop(zeroCenter ? parsed + Math.PI : parsed, Math.PI * 2);
return zeroCenter ? MathUtils.clamp(parsed - Math.PI, -Math.PI / (halfCircle ? 2 : 1), Math.PI / (halfCircle ? 2 : 1)) : parsed;
}
/**
* @summary Creates a THREE texture from an image
* @memberOf PSV.utils
* @param {HTMLImageElement | HTMLCanvasElement} img
* @return {external:THREE.Texture}
*/
export function createTexture(img) {
const texture = new Texture(img);
texture.needsUpdate = true;
texture.minFilter = LinearFilter;
texture.generateMipmaps = false;
return texture;
}
const quaternion = new Quaternion();
/**
* @summary Applies the inverse of Euler angles to a vector
* @memberOf PSV.utils
* @param {external:THREE.Vector3} vector
* @param {external:THREE.Euler} euler
*/
export function applyEulerInverse(vector, euler) {
quaternion.setFromEuler(euler).invert();
vector.applyQuaternion(quaternion);
}