@shapediver/viewer.shared.services 2.10.0 → 2.11.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/dist/converter/Converter.d.ts +15 -7
- package/dist/converter/Converter.d.ts.map +1 -1
- package/dist/converter/Converter.js +63 -52
- package/dist/converter/Converter.js.map +1 -1
- package/dist/dom-event-engine/DomEventEngine.js +9 -5
- package/dist/dom-event-engine/DomEventEngine.js.map +1 -1
- package/dist/dom-event-engine/IDomEventListener.js +2 -1
- package/dist/event-engine/EventEngine.js +16 -12
- package/dist/event-engine/EventEngine.js.map +1 -1
- package/dist/event-engine/EventTypes.js +18 -15
- package/dist/event-engine/EventTypes.js.map +1 -1
- package/dist/event-engine/interfaces/ICallback.js +2 -1
- package/dist/event-engine/interfaces/IEvent.js +2 -1
- package/dist/event-engine/interfaces/IEvent.js.map +1 -1
- package/dist/event-engine/interfaces/IListener.js +2 -1
- package/dist/http-client/HttpClient.d.ts +75 -2
- package/dist/http-client/HttpClient.d.ts.map +1 -1
- package/dist/http-client/HttpClient.js +200 -27
- package/dist/http-client/HttpClient.js.map +1 -1
- package/dist/http-client/HttpResponse.d.ts +1 -0
- package/dist/http-client/HttpResponse.d.ts.map +1 -1
- package/dist/http-client/HttpResponse.js +2 -1
- package/dist/index.js +80 -32
- package/dist/index.js.map +1 -1
- package/dist/input-validator/InputValidator.js +16 -9
- package/dist/input-validator/InputValidator.js.map +1 -1
- package/dist/logger/ErrorTypeGuards.js +55 -35
- package/dist/logger/ErrorTypeGuards.js.map +1 -1
- package/dist/logger/Logger.js +7 -3
- package/dist/logger/Logger.js.map +1 -1
- package/dist/logger/ShapeDiverBackendErrors.js +13 -7
- package/dist/logger/ShapeDiverBackendErrors.js.map +1 -1
- package/dist/logger/ShapeDiverError.js +9 -4
- package/dist/logger/ShapeDiverError.js.map +1 -1
- package/dist/logger/ShapeDiverViewerErrors.js +40 -25
- package/dist/logger/ShapeDiverViewerErrors.js.map +1 -1
- package/dist/performance-evaluator/PerformanceEvaluator.d.ts +13 -0
- package/dist/performance-evaluator/PerformanceEvaluator.d.ts.map +1 -1
- package/dist/performance-evaluator/PerformanceEvaluator.js +39 -3
- package/dist/performance-evaluator/PerformanceEvaluator.js.map +1 -1
- package/dist/settings-engine/SettingsEngine.js +20 -16
- package/dist/settings-engine/SettingsEngine.js.map +1 -1
- package/dist/state-engine/StateEngine.js +8 -4
- package/dist/state-engine/StateEngine.js.map +1 -1
- package/dist/state-engine/StatePromise.js +5 -1
- package/dist/state-engine/StatePromise.js.map +1 -1
- package/dist/system-info/SystemInfo.js +10 -3
- package/dist/system-info/SystemInfo.js.map +1 -1
- package/dist/type-check/TypeChecker.js +5 -1
- package/dist/type-check/TypeChecker.js.map +1 -1
- package/dist/uuid-generator/UuidGenerator.js +10 -6
- package/dist/uuid-generator/UuidGenerator.js.map +1 -1
- package/package.json +4 -4
- package/src/converter/Converter.ts +69 -62
- package/src/http-client/HttpClient.ts +224 -31
- package/src/http-client/HttpResponse.ts +3 -1
- package/src/performance-evaluator/PerformanceEvaluator.ts +33 -3
- package/src/settings-engine/SettingsEngine.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { vec3
|
|
2
|
-
import { TinyColor } from '@ctrl/tinycolor'
|
|
1
|
+
import { vec3 } from 'gl-matrix';
|
|
2
|
+
import { ColorInput, TinyColor } from '@ctrl/tinycolor';
|
|
3
3
|
import { HttpClient } from '../http-client/HttpClient';
|
|
4
4
|
import { HttpResponse } from '../http-client/HttpResponse';
|
|
5
5
|
|
|
@@ -29,22 +29,22 @@ export class Converter {
|
|
|
29
29
|
reader.onerror = reject;
|
|
30
30
|
reader.readAsDataURL(blob);
|
|
31
31
|
});
|
|
32
|
-
data = data.replace('data:image/svg+xml;base64,', '')
|
|
32
|
+
data = data.replace('data:image/svg+xml;base64,', '');
|
|
33
33
|
data = atob(data);
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
const svgC = document.createElement('DIV');
|
|
36
36
|
svgC.id = 'svgc';
|
|
37
37
|
svgC.innerHTML = <string>data;
|
|
38
38
|
|
|
39
39
|
// now we can access the svg element as a DOM object
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const svgE = svgC.getElementsByTagName('svg');
|
|
41
|
+
const childImageURIs: string[] = [];
|
|
42
42
|
let styleURIs: string[] = [];
|
|
43
43
|
|
|
44
44
|
// collect image urls
|
|
45
45
|
for (let i = 0; i < svgE.length; ++i) {
|
|
46
46
|
for (let j = 0; j < 2; ++j) {
|
|
47
|
-
|
|
47
|
+
const childImages = <HTMLCollectionOf<SVGImageElement>>svgE[i].getElementsByTagName(['image', 'img'][j]);
|
|
48
48
|
for (let k = 0; k < childImages.length; ++k) {
|
|
49
49
|
if (childImages[k].href.baseVal.substring(0, 5) != 'data:') {
|
|
50
50
|
childImageURIs.push(childImages[k].href.baseVal);
|
|
@@ -54,9 +54,9 @@ export class Converter {
|
|
|
54
54
|
// collect potential font definitions
|
|
55
55
|
// we assume styles are imported using the following syntax:
|
|
56
56
|
// @import url(CSS_URL);
|
|
57
|
-
|
|
57
|
+
const styleElements = <HTMLCollectionOf<HTMLStyleElement>>svgE[i].getElementsByTagName('style');
|
|
58
58
|
for (let j = 0; j < styleElements.length; ++j) {
|
|
59
|
-
|
|
59
|
+
const regex = /@import\x20url\(\s*(.*?)\s*\);/g;
|
|
60
60
|
let m;
|
|
61
61
|
while ((m = regex.exec(styleElements[j].innerHTML)) !== null) {
|
|
62
62
|
styleURIs.push(m[1]);
|
|
@@ -71,17 +71,18 @@ export class Converter {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// creating a promise for each image which needs to be converted to a data URI
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
const replacementPromises = [];
|
|
75
|
+
const createImagePromise = async (uri: string) => {
|
|
76
76
|
if (uri.length > 0) {
|
|
77
|
-
const response = await this._httpClient.
|
|
78
|
-
|
|
79
|
-
let
|
|
77
|
+
const response = await this._httpClient.get(uri, undefined, true) as HttpResponse<ArrayBuffer>;
|
|
78
|
+
const uInt8Array = new Uint8Array(response.data as ArrayBuffer);
|
|
79
|
+
let i = uInt8Array.length;
|
|
80
|
+
const biStr = []; //new Array(i);
|
|
80
81
|
while (i--)
|
|
81
82
|
biStr[i] = String.fromCharCode(uInt8Array[i]);
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const base64Data = window.btoa(biStr.join(''));
|
|
85
|
+
const imgDataUrl = 'data:' + response.headers['content-type'] + ';base64,' + base64Data;
|
|
85
86
|
|
|
86
87
|
// replace url in SVG string
|
|
87
88
|
// CAUTION theoretically this could cause unwanted replacements
|
|
@@ -93,34 +94,35 @@ export class Converter {
|
|
|
93
94
|
replacementPromises.push(createImagePromise(childImageURIs[i]));
|
|
94
95
|
|
|
95
96
|
// now we create promises for the google fonts to be imported
|
|
96
|
-
|
|
97
|
+
const createStylePromise = async (styleUrl: string) => {
|
|
97
98
|
const response = await this._httpClient.get(
|
|
98
99
|
styleUrl,
|
|
99
100
|
{ responseType: 'text' }
|
|
100
|
-
)
|
|
101
|
-
let cssString = response.data;
|
|
101
|
+
) as HttpResponse<ArrayBuffer>;
|
|
102
|
+
let cssString = response.data as unknown as string;
|
|
102
103
|
// we assume that fonts are imported using the following syntax:
|
|
103
104
|
// url(FONT_URI);
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
const fontURLs = [];
|
|
106
|
+
const regex = /url\(\s*(.*?)\s*\)/g;
|
|
106
107
|
let m;
|
|
107
108
|
while ((m = regex.exec(cssString)) !== null) {
|
|
108
109
|
fontURLs.push(m[1]);
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
const fontPromises = [];
|
|
113
|
+
const createFontPromise = async (fUrl: string) => {
|
|
113
114
|
const response = await this._httpClient.get(
|
|
114
115
|
fUrl,
|
|
115
116
|
{ responseType: 'arraybuffer' }
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
let
|
|
117
|
+
) as HttpResponse<ArrayBuffer>;
|
|
118
|
+
const uInt8Array = new Uint8Array(response.data as ArrayBuffer);
|
|
119
|
+
let i = uInt8Array.length;
|
|
120
|
+
const biStr = []; //new Array(i);
|
|
119
121
|
while (i--)
|
|
120
122
|
biStr[i] = String.fromCharCode(uInt8Array[i]);
|
|
121
123
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
const base64Data = window.btoa(biStr.join(''));
|
|
125
|
+
const fontDataUrl = 'data:' + response.headers['content-type'] + ';base64,' + base64Data;
|
|
124
126
|
if (fUrl.length > 0)
|
|
125
127
|
cssString = cssString.replace(fUrl, fontDataUrl);
|
|
126
128
|
};
|
|
@@ -137,19 +139,22 @@ export class Converter {
|
|
|
137
139
|
|
|
138
140
|
await Promise.all(replacementPromises);
|
|
139
141
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
const du = 'data:image/svg+xml,' + encodeURIComponent(data);
|
|
143
|
+
const img = new Image(); // same as document.createElement('img')
|
|
142
144
|
img.crossOrigin = 'Anonymous';
|
|
143
145
|
const promise = new Promise<void>((resolve, reject) => {
|
|
144
146
|
img.onload = () => resolve();
|
|
145
147
|
img.onerror = reject;
|
|
146
|
-
})
|
|
148
|
+
});
|
|
147
149
|
img.src = du;
|
|
148
150
|
await promise;
|
|
149
151
|
return img;
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
public async responseToImage(response: HttpResponse<ArrayBuffer>): Promise<HTMLImageElement> {
|
|
154
|
+
public async responseToImage(response: HttpResponse<ArrayBuffer | HTMLImageElement>): Promise<HTMLImageElement> {
|
|
155
|
+
// if we already receive and image, this conversion already happened
|
|
156
|
+
if(response.data instanceof HTMLImageElement) return response.data;
|
|
157
|
+
|
|
153
158
|
const arrayBufferView = new Uint8Array( response.data );
|
|
154
159
|
const blob = new Blob([ arrayBufferView ], { type: response.headers['content-type'] } );
|
|
155
160
|
if (response.headers['content-type'] === 'image/svg+xml') {
|
|
@@ -160,8 +165,8 @@ export class Converter {
|
|
|
160
165
|
const promise = new Promise<void>((resolve, reject) => {
|
|
161
166
|
img.onload = () => resolve();
|
|
162
167
|
img.onerror = reject;
|
|
163
|
-
})
|
|
164
|
-
img.crossOrigin =
|
|
168
|
+
});
|
|
169
|
+
img.crossOrigin = 'anonymous';
|
|
165
170
|
img.src = URL.createObjectURL(blob);
|
|
166
171
|
await promise;
|
|
167
172
|
URL.revokeObjectURL(img.src);
|
|
@@ -169,17 +174,17 @@ export class Converter {
|
|
|
169
174
|
}
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
public toAlpha(color:
|
|
177
|
+
public toAlpha(color: unknown): number {
|
|
173
178
|
const c = this.toHexColor(color);
|
|
174
179
|
if (c.length <= 8) return 1;
|
|
175
180
|
return parseInt(c.slice(c.length - 2, c.length), 16) / 255;
|
|
176
181
|
}
|
|
177
182
|
|
|
178
|
-
public toColorArray(color:
|
|
179
|
-
if(typeof color !== 'string' || !color.startsWith(
|
|
183
|
+
public toColorArray(color: unknown): number[] {
|
|
184
|
+
if(typeof color !== 'string' || !color.startsWith('#'))
|
|
180
185
|
color = this.toHexColor(color);
|
|
181
|
-
const tColor = new TinyColor(color);
|
|
182
|
-
const rgb = tColor.toRgb()
|
|
186
|
+
const tColor = new TinyColor(color as ColorInput | undefined);
|
|
187
|
+
const rgb = tColor.toRgb();
|
|
183
188
|
return [rgb.r / 255.0, rgb.g / 255.0, rgb.b / 255.0];
|
|
184
189
|
}
|
|
185
190
|
|
|
@@ -187,7 +192,7 @@ export class Converter {
|
|
|
187
192
|
* @param color
|
|
188
193
|
* @param defColor
|
|
189
194
|
*/
|
|
190
|
-
public toHex8Color(color:
|
|
195
|
+
public toHex8Color(color: unknown, defColorString: string = '#000'): string {
|
|
191
196
|
const c = this.toHexColor(color, defColorString);
|
|
192
197
|
const tColor = new TinyColor(c);
|
|
193
198
|
const cH8 = tColor.toHex8String();
|
|
@@ -201,13 +206,13 @@ export class Converter {
|
|
|
201
206
|
* @param color
|
|
202
207
|
* @param defColor
|
|
203
208
|
*/
|
|
204
|
-
public toHexColor(color:
|
|
209
|
+
public toHexColor(color: unknown, defColorString: string = '#000'): string {
|
|
205
210
|
if (!color || color === 'default') return defColorString;
|
|
206
211
|
|
|
207
|
-
if (color.constructor === Float32Array)
|
|
208
|
-
color = Array.from(color);
|
|
212
|
+
if ((color as Float32Array).constructor === Float32Array)
|
|
213
|
+
color = Array.from(color as Float32Array);
|
|
209
214
|
|
|
210
|
-
const tColor = new TinyColor(color);
|
|
215
|
+
const tColor = new TinyColor(color as TinyColor | undefined);
|
|
211
216
|
|
|
212
217
|
if (color instanceof TinyColor)
|
|
213
218
|
return this.tinyColorToString(tColor);
|
|
@@ -215,17 +220,17 @@ export class Converter {
|
|
|
215
220
|
// check if we got a number
|
|
216
221
|
if (typeof color === 'number') {
|
|
217
222
|
let cs = color.toString(16);
|
|
218
|
-
|
|
223
|
+
const cl = cs.length;
|
|
219
224
|
if (cl < 3) cs = cs.padStart(3, '0');
|
|
220
225
|
else if (cl < 6) cs = cs.padStart(6, '0');
|
|
221
226
|
else if (cl < 8) cs = cs.padEnd(8, '0');
|
|
222
|
-
|
|
227
|
+
const tc = new TinyColor(cs);
|
|
223
228
|
return tc.isValid ? this.tinyColorToString(tc) : defColorString;
|
|
224
229
|
}
|
|
225
230
|
|
|
226
231
|
// check if the input is a THREE.Color
|
|
227
|
-
if (color.isColor && typeof color.getHexString == 'function') {
|
|
228
|
-
|
|
232
|
+
if ((color as THREE.Color).isColor && typeof (color as THREE.Color).getHexString == 'function') {
|
|
233
|
+
const tc = new TinyColor((color as THREE.Color).getHexString());
|
|
229
234
|
return tc.isValid ? this.tinyColorToString(tc) : defColorString;
|
|
230
235
|
}
|
|
231
236
|
|
|
@@ -241,13 +246,13 @@ export class Converter {
|
|
|
241
246
|
if (!isRGBArray)
|
|
242
247
|
return defColorString;
|
|
243
248
|
|
|
244
|
-
|
|
249
|
+
const tc = new TinyColor({
|
|
245
250
|
r: Math.max(0, Math.min(color[0], 255)),
|
|
246
251
|
g: Math.max(0, Math.min(color[1], 255)),
|
|
247
252
|
b: Math.max(0, Math.min(color[2], 255))
|
|
248
253
|
});
|
|
249
254
|
if (color.length == 4) {
|
|
250
|
-
|
|
255
|
+
const a = parseFloat(color[3]);
|
|
251
256
|
if (!isNaN(a)) {
|
|
252
257
|
tc.setAlpha(Math.max(0, Math.min(a, 255)) / 255);
|
|
253
258
|
}
|
|
@@ -258,50 +263,52 @@ export class Converter {
|
|
|
258
263
|
// if we got something other than a string, check if
|
|
259
264
|
// tinycolor can work with it
|
|
260
265
|
if (typeof color !== 'string') {
|
|
261
|
-
|
|
266
|
+
const tc = new TinyColor(color as string);
|
|
262
267
|
return tc.isValid ? this.tinyColorToString(tc) : defColorString;
|
|
263
268
|
}
|
|
264
269
|
|
|
265
270
|
// tinycolor doesn't like 0x
|
|
266
|
-
|
|
271
|
+
const tmpColor = color.replace('0x', '#');
|
|
267
272
|
|
|
268
273
|
// if we got no alpha value, add full opacity
|
|
269
274
|
if (tmpColor.match(/^#[a-f0-9]{6}$/i) !== null) {
|
|
270
|
-
|
|
275
|
+
const tc = new TinyColor(tmpColor + 'ff');
|
|
271
276
|
return tc.isValid ? this.tinyColorToString(tc) : defColorString;
|
|
272
277
|
}
|
|
273
278
|
|
|
274
279
|
// standard case
|
|
275
280
|
if (tmpColor.match(/^#[a-f0-9]{8}$/i) !== null) {
|
|
276
|
-
|
|
281
|
+
const tc = new TinyColor(tmpColor);
|
|
277
282
|
return tc.isValid ? this.tinyColorToString(tc) : defColorString;
|
|
278
283
|
}
|
|
279
284
|
|
|
280
285
|
// correct number which have the alpha value defined as a single hex digit
|
|
281
286
|
if (tmpColor.match(/^#[a-f0-9]{7}$/i) !== null) {
|
|
282
|
-
|
|
287
|
+
const tc = new TinyColor(tmpColor.slice(0, 7) + '0' + tmpColor.slice(-1));
|
|
283
288
|
return tc.isValid ? this.tinyColorToString(tc) : defColorString;
|
|
284
289
|
}
|
|
285
290
|
|
|
286
291
|
// check if tinycolor understands the string
|
|
287
|
-
|
|
292
|
+
const tc = new TinyColor(tmpColor);
|
|
288
293
|
return tc.isValid ? this.tinyColorToString(tc) : defColorString;
|
|
289
294
|
}
|
|
290
295
|
|
|
291
|
-
public toThreeJsColorInput(color:
|
|
296
|
+
public toThreeJsColorInput(color: unknown): string {
|
|
292
297
|
const c = this.toHexColor(color);
|
|
293
298
|
return c.slice(0, c.length - 2);
|
|
294
299
|
}
|
|
295
300
|
|
|
296
|
-
public toVec3(point:
|
|
301
|
+
public toVec3(point: vec3 | { x: number, y: number, z: number} | { X: number, Y: number, Z: number}): vec3 {
|
|
297
302
|
if (Array.isArray(point) && point.length >= 3 && typeof point[0] === 'number' && typeof point[1] === 'number' && typeof point[2] === 'number')
|
|
298
303
|
return vec3.fromValues(point[0], point[1], point[2]);
|
|
299
304
|
|
|
300
|
-
|
|
301
|
-
|
|
305
|
+
const pointCast1 = point as { x: number, y: number, z: number};
|
|
306
|
+
if (((pointCast1.x || pointCast1.x === 0) && typeof pointCast1.x === 'number') && ((pointCast1.y || pointCast1.y === 0) && typeof pointCast1.y === 'number') && ((pointCast1.z || pointCast1.z === 0) && typeof pointCast1.z === 'number'))
|
|
307
|
+
return vec3.fromValues(pointCast1.x, pointCast1.y, pointCast1.z);
|
|
302
308
|
|
|
303
|
-
|
|
304
|
-
|
|
309
|
+
const pointCast2 = point as { X: number, Y: number, Z: number};
|
|
310
|
+
if (((pointCast2.X || pointCast2.X === 0) && typeof pointCast2.X === 'number') && ((pointCast2.Y || pointCast2.Y === 0) && typeof pointCast2.Y === 'number') && ((pointCast2.Z || pointCast2.Z === 0) && typeof pointCast2.Z === 'number'))
|
|
311
|
+
return vec3.fromValues(pointCast2.X, pointCast2.Y, pointCast2.Z);
|
|
305
312
|
|
|
306
313
|
return vec3.create();
|
|
307
314
|
}
|
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
import axios, { AxiosRequestConfig } from 'axios'
|
|
2
|
-
import { ShapeDiverError as ShapeDiverBackendError, isGBResponseError, isGBRequestError, isGBError } from '@shapediver/sdk.geometry-api-sdk-v2'
|
|
1
|
+
import axios, { AxiosRequestConfig } from 'axios';
|
|
2
|
+
import { ShapeDiverError as ShapeDiverBackendError, isGBResponseError, isGBRequestError, isGBError } from '@shapediver/sdk.geometry-api-sdk-v2';
|
|
3
3
|
import { ShapeDiverGeometryBackendError, ShapeDiverGeometryBackendRequestError, ShapeDiverGeometryBackendResponseError } from '../logger/ShapeDiverBackendErrors';
|
|
4
4
|
import { HttpResponse } from './HttpResponse';
|
|
5
|
+
import { Converter } from '../converter/Converter';
|
|
5
6
|
|
|
6
7
|
export class HttpClient {
|
|
7
|
-
// #region Properties (
|
|
8
|
-
|
|
8
|
+
// #region Properties (7)
|
|
9
9
|
private static _instance: HttpClient;
|
|
10
10
|
|
|
11
|
+
private _dataCache: Map<
|
|
12
|
+
string,
|
|
13
|
+
{
|
|
14
|
+
value: Promise<HttpResponse<unknown>>,
|
|
15
|
+
timestamp: number,
|
|
16
|
+
resolved: boolean,
|
|
17
|
+
size?: number
|
|
18
|
+
}> = new Map();
|
|
19
|
+
private _enableCaching: boolean = true;
|
|
20
|
+
private _excludedQueryParameters: string[] = ['Expires', 'Signature', 'Key-Pair-Id'];
|
|
21
|
+
private _maxCacheSize: number = 1024 * 1024 * 32;
|
|
11
22
|
private _sessionLoading: {
|
|
12
23
|
[key: string]: {
|
|
13
24
|
getAsset: (url: string) => Promise<[ArrayBuffer, string, string]>,
|
|
@@ -15,10 +26,11 @@ export class HttpClient {
|
|
|
15
26
|
}
|
|
16
27
|
} = {};
|
|
17
28
|
|
|
18
|
-
// #endregion Properties (
|
|
29
|
+
// #endregion Properties (7)
|
|
19
30
|
|
|
20
31
|
// #region Constructors (1)
|
|
21
32
|
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
22
34
|
private constructor() { }
|
|
23
35
|
|
|
24
36
|
// #endregion Constructors (1)
|
|
@@ -31,8 +43,44 @@ export class HttpClient {
|
|
|
31
43
|
|
|
32
44
|
// #endregion Public Static Accessors (1)
|
|
33
45
|
|
|
46
|
+
// #region Public Accessors (6)
|
|
47
|
+
|
|
48
|
+
public get enableCaching(): boolean {
|
|
49
|
+
return this._enableCaching;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public set enableCaching(value: boolean) {
|
|
53
|
+
this._enableCaching = value;
|
|
54
|
+
if (this._enableCaching === false)
|
|
55
|
+
this._dataCache.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public get excludedQueryParameters(): string[] {
|
|
59
|
+
return this._excludedQueryParameters;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public set excludedQueryParameters(value: string[]) {
|
|
63
|
+
this._excludedQueryParameters = value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public get maxCacheSize(): number {
|
|
67
|
+
return this._maxCacheSize;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public set maxCacheSize(value: number) {
|
|
71
|
+
this._maxCacheSize = value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// #endregion Public Accessors (6)
|
|
75
|
+
|
|
34
76
|
// #region Public Methods (5)
|
|
35
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Add the data loading options from a session.
|
|
80
|
+
*
|
|
81
|
+
* @param sessionId
|
|
82
|
+
* @param callbacks
|
|
83
|
+
*/
|
|
36
84
|
public addDataLoading(sessionId: string, callbacks: {
|
|
37
85
|
getAsset: (url: string) => Promise<[ArrayBuffer, string, string]>,
|
|
38
86
|
downloadTexture: (sessionId: string, url: string) => Promise<[ArrayBuffer, string]>,
|
|
@@ -53,13 +101,30 @@ export class HttpClient {
|
|
|
53
101
|
if (isGBResponseError(e)) {
|
|
54
102
|
throw new ShapeDiverGeometryBackendResponseError(e.message, e.status, e.error, e.desc);
|
|
55
103
|
} else if (isGBRequestError(e)) {
|
|
56
|
-
throw new ShapeDiverGeometryBackendRequestError(e.message, e.desc)
|
|
57
|
-
} else if(isGBError(e)) {
|
|
58
|
-
throw new ShapeDiverGeometryBackendError(e.message)
|
|
104
|
+
throw new ShapeDiverGeometryBackendRequestError(e.message, e.desc);
|
|
105
|
+
} else if (isGBError(e)) {
|
|
106
|
+
throw new ShapeDiverGeometryBackendError(e.message);
|
|
59
107
|
}
|
|
60
108
|
}
|
|
61
109
|
|
|
62
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Get the requested resource either as a download or from the cache.
|
|
112
|
+
* If available, the registered session loading is used for download.
|
|
113
|
+
* Textures are downloaded via a specific endpoint and can be converted in this step as well.
|
|
114
|
+
* Depending on the provided caching options, the requested resource might already be cached.
|
|
115
|
+
*
|
|
116
|
+
* @param href
|
|
117
|
+
* @param config
|
|
118
|
+
* @param textureLoading
|
|
119
|
+
* @param textureConversion
|
|
120
|
+
* @returns
|
|
121
|
+
*/
|
|
122
|
+
public async get(href: string, config: AxiosRequestConfig = { responseType: 'arraybuffer' }, textureLoading: boolean = false, textureConversion: boolean = false): Promise<HttpResponse<unknown>> {
|
|
123
|
+
const dataKey = this.hrefToDataKey(href);
|
|
124
|
+
|
|
125
|
+
// return element if it exists in cache
|
|
126
|
+
if (this._dataCache.has(dataKey)) return this.getFromCache(dataKey);
|
|
127
|
+
|
|
63
128
|
// try to get sessionId from href
|
|
64
129
|
let sessionId = this.getSessionId(href);
|
|
65
130
|
|
|
@@ -75,35 +140,66 @@ export class HttpClient {
|
|
|
75
140
|
if (sessionId)
|
|
76
141
|
sessionLoading = this._sessionLoading[sessionId];
|
|
77
142
|
|
|
143
|
+
let loadingPromise;
|
|
144
|
+
|
|
78
145
|
// separation texture vs everything else
|
|
79
146
|
if (textureLoading) {
|
|
80
147
|
// if we have a sessionId and the sessionLoading functions and the image is not a blob or data, we load it via the sdk
|
|
81
148
|
if (sessionLoading !== undefined && sessionId !== undefined && !href.startsWith('blob:') && !href.startsWith('data:')) {
|
|
82
149
|
// take first session to load a texture that is not session related
|
|
83
|
-
|
|
84
|
-
sessionLoading!.downloadTexture(sessionId!, href).then((result) => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
150
|
+
loadingPromise = new Promise<HttpResponse<ArrayBuffer | HTMLImageElement>>((resolve, reject) => {
|
|
151
|
+
sessionLoading!.downloadTexture(sessionId!, href).then(async (result) => {
|
|
152
|
+
if (textureConversion) {
|
|
153
|
+
const image = await Converter.instance.responseToImage({
|
|
154
|
+
data: result[0],
|
|
155
|
+
headers: {
|
|
156
|
+
'content-type': result[1]
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
resolve({
|
|
160
|
+
data: image,
|
|
161
|
+
size: result[0].byteLength,
|
|
162
|
+
headers: {
|
|
163
|
+
'content-type': result[1]
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
resolve({
|
|
168
|
+
data: result[0],
|
|
169
|
+
headers: {
|
|
170
|
+
'content-type': result[1]
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
}).catch(e => reject(e));
|
|
176
|
+
}).catch(e => { throw this.convertError(e); });
|
|
93
177
|
} else {
|
|
94
178
|
// we can load blobs and data urls directly
|
|
95
179
|
// or load it directly if we don't have a session
|
|
96
|
-
|
|
97
|
-
.
|
|
180
|
+
loadingPromise = axios(href, Object.assign({ method: 'get' }, config))
|
|
181
|
+
.then(async (result) => {
|
|
182
|
+
if (textureConversion) {
|
|
183
|
+
const image = await Converter.instance.responseToImage(result);
|
|
184
|
+
return {
|
|
185
|
+
data: image,
|
|
186
|
+
size: result.data.byteLength,
|
|
187
|
+
headers: result.headers
|
|
188
|
+
};
|
|
189
|
+
} else {
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
.catch(e => { throw this.convertError(e); });
|
|
98
194
|
}
|
|
99
195
|
} else {
|
|
100
196
|
if (!sessionLoading) {
|
|
101
197
|
// if there is no session to load from, we use the fallback option
|
|
102
|
-
|
|
103
|
-
.catch(e => { throw this.convertError(e) });
|
|
198
|
+
loadingPromise = axios(href, Object.assign({ method: 'get' }, config))
|
|
199
|
+
.catch(e => { throw this.convertError(e); });
|
|
104
200
|
} else {
|
|
105
201
|
// all data links where we could somehow find a session to load it with
|
|
106
|
-
|
|
202
|
+
loadingPromise = new Promise<HttpResponse<ArrayBuffer>>((resolve, reject) => {
|
|
107
203
|
sessionLoading!.getAsset(href)
|
|
108
204
|
.then((result) => {
|
|
109
205
|
resolve({
|
|
@@ -111,31 +207,104 @@ export class HttpClient {
|
|
|
111
207
|
headers: {
|
|
112
208
|
'content-type': result[1]
|
|
113
209
|
}
|
|
114
|
-
})
|
|
210
|
+
});
|
|
115
211
|
})
|
|
116
212
|
.catch(() => {
|
|
117
213
|
// if this fails, we just load it directly
|
|
118
214
|
const axiosPromise = axios(href, Object.assign({ method: 'get' }, config));
|
|
119
|
-
axiosPromise.catch(e => reject(e))
|
|
215
|
+
axiosPromise.catch(e => reject(e));
|
|
120
216
|
resolve(axiosPromise);
|
|
121
217
|
});
|
|
122
|
-
}).catch(e => { throw this.convertError(e) });
|
|
218
|
+
}).catch(e => { throw this.convertError(e); });
|
|
123
219
|
}
|
|
124
220
|
}
|
|
221
|
+
|
|
222
|
+
if (this.enableCaching)
|
|
223
|
+
this.addToCache(dataKey, loadingPromise);
|
|
224
|
+
|
|
225
|
+
return loadingPromise;
|
|
125
226
|
}
|
|
126
227
|
|
|
127
|
-
|
|
128
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Get the requested texture either as a download or from the cache.
|
|
230
|
+
*
|
|
231
|
+
* @param href
|
|
232
|
+
* @returns
|
|
233
|
+
*/
|
|
234
|
+
public async loadTexture(href: string): Promise<HttpResponse<HTMLImageElement>> {
|
|
235
|
+
return this.get(href, undefined, true, true) as Promise<HttpResponse<HTMLImageElement>>;
|
|
129
236
|
}
|
|
130
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Add the data loading options from a session.
|
|
240
|
+
*
|
|
241
|
+
* @param sessionId
|
|
242
|
+
*/
|
|
131
243
|
public removeDataLoading(sessionId: string) {
|
|
132
244
|
delete this._sessionLoading[sessionId];
|
|
133
245
|
}
|
|
134
246
|
|
|
135
247
|
// #endregion Public Methods (5)
|
|
136
248
|
|
|
137
|
-
// #region Private Methods (
|
|
249
|
+
// #region Private Methods (5)
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Add
|
|
253
|
+
*
|
|
254
|
+
* @param key
|
|
255
|
+
* @param value
|
|
256
|
+
*/
|
|
257
|
+
private addToCache(key: string, value: Promise<HttpResponse<unknown>>) {
|
|
258
|
+
// Remove items from the cache until the cache size is smaller than the maximum cache size.
|
|
259
|
+
// Only resolved promises are evaluated, as unresolved promises don't add any size.
|
|
260
|
+
while (this.calculateCacheSize() >= this._maxCacheSize) {
|
|
261
|
+
// Remove the oldest entry if the cache is full
|
|
262
|
+
const oldestKey = this._dataCache.keys().next().value;
|
|
263
|
+
this._dataCache.delete(oldestKey);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const timestamp = Date.now();
|
|
267
|
+
this._dataCache.set(key, { value, timestamp, resolved: false });
|
|
268
|
+
|
|
269
|
+
// once the promise resolves, set resolved and size properties
|
|
270
|
+
value.then((promiseResult) => {
|
|
271
|
+
const size = promiseResult.size ? promiseResult.size : (promiseResult.data as ArrayBuffer).byteLength;
|
|
272
|
+
this._dataCache.set(key, { value, timestamp, resolved: true, size });
|
|
273
|
+
}).catch(e => { throw this.convertError(e); });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Calculate the current cache size from all resolved promises.
|
|
278
|
+
*
|
|
279
|
+
* @returns
|
|
280
|
+
*/
|
|
281
|
+
private calculateCacheSize() {
|
|
282
|
+
let size = 0;
|
|
283
|
+
this._dataCache.forEach(value => {
|
|
284
|
+
if (value.resolved === true)
|
|
285
|
+
size += value.size!;
|
|
286
|
+
});
|
|
287
|
+
return size;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get the value of an object from the cache.
|
|
292
|
+
*
|
|
293
|
+
* @param key
|
|
294
|
+
* @returns
|
|
295
|
+
*/
|
|
296
|
+
private getFromCache(key: string): Promise<HttpResponse<unknown>> {
|
|
297
|
+
const cachedObject = this._dataCache.get(key)!;
|
|
298
|
+
this._dataCache.set(key, { value: cachedObject.value, timestamp: Date.now(), resolved: cachedObject.resolved, size: cachedObject.size });
|
|
299
|
+
return cachedObject.value;
|
|
300
|
+
}
|
|
138
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Get the session id of the provided href.
|
|
304
|
+
*
|
|
305
|
+
* @param href
|
|
306
|
+
* @returns
|
|
307
|
+
*/
|
|
139
308
|
private getSessionId(href: string): string | undefined {
|
|
140
309
|
// searching for "/session/SESSION_ID/{'output' | 'export' | 'texture'}/ASSET_DATA"
|
|
141
310
|
const parts = href.split('/');
|
|
@@ -151,5 +320,29 @@ export class HttpClient {
|
|
|
151
320
|
return;
|
|
152
321
|
}
|
|
153
322
|
|
|
154
|
-
|
|
323
|
+
/**
|
|
324
|
+
* Convert the provided href to a data cache key.
|
|
325
|
+
* In this conversion the excludedQueryParameters are removed from the href.
|
|
326
|
+
*
|
|
327
|
+
* @param href
|
|
328
|
+
* @returns
|
|
329
|
+
*/
|
|
330
|
+
private hrefToDataKey(href: string) {
|
|
331
|
+
const url = new URL(href);
|
|
332
|
+
|
|
333
|
+
// Create a URLSearchParams object from the existing query parameters
|
|
334
|
+
const params = new URLSearchParams(url.search);
|
|
335
|
+
|
|
336
|
+
for (let i = 0; i < this._excludedQueryParameters.length; i++)
|
|
337
|
+
// Remove specific query parameters
|
|
338
|
+
params.delete(this._excludedQueryParameters[i]);
|
|
339
|
+
|
|
340
|
+
// Reconstruct the URL with the modified query parameters
|
|
341
|
+
url.search = params.toString();
|
|
342
|
+
|
|
343
|
+
const hrefAsKey = url.toString();
|
|
344
|
+
return window.btoa(hrefAsKey);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// #endregion Private Methods (5)
|
|
155
348
|
}
|