@shapediver/viewer.shared.services 2.10.1-rc.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.
Files changed (58) hide show
  1. package/dist/converter/Converter.d.ts +15 -7
  2. package/dist/converter/Converter.d.ts.map +1 -1
  3. package/dist/converter/Converter.js +63 -52
  4. package/dist/converter/Converter.js.map +1 -1
  5. package/dist/dom-event-engine/DomEventEngine.js +9 -5
  6. package/dist/dom-event-engine/DomEventEngine.js.map +1 -1
  7. package/dist/dom-event-engine/IDomEventListener.js +2 -1
  8. package/dist/event-engine/EventEngine.js +16 -12
  9. package/dist/event-engine/EventEngine.js.map +1 -1
  10. package/dist/event-engine/EventTypes.js +18 -15
  11. package/dist/event-engine/EventTypes.js.map +1 -1
  12. package/dist/event-engine/interfaces/ICallback.js +2 -1
  13. package/dist/event-engine/interfaces/IEvent.js +2 -1
  14. package/dist/event-engine/interfaces/IEvent.js.map +1 -1
  15. package/dist/event-engine/interfaces/IListener.js +2 -1
  16. package/dist/http-client/HttpClient.d.ts +75 -2
  17. package/dist/http-client/HttpClient.d.ts.map +1 -1
  18. package/dist/http-client/HttpClient.js +200 -27
  19. package/dist/http-client/HttpClient.js.map +1 -1
  20. package/dist/http-client/HttpResponse.d.ts +1 -0
  21. package/dist/http-client/HttpResponse.d.ts.map +1 -1
  22. package/dist/http-client/HttpResponse.js +2 -1
  23. package/dist/index.js +80 -32
  24. package/dist/index.js.map +1 -1
  25. package/dist/input-validator/InputValidator.js +16 -9
  26. package/dist/input-validator/InputValidator.js.map +1 -1
  27. package/dist/logger/ErrorTypeGuards.js +55 -35
  28. package/dist/logger/ErrorTypeGuards.js.map +1 -1
  29. package/dist/logger/Logger.js +7 -3
  30. package/dist/logger/Logger.js.map +1 -1
  31. package/dist/logger/ShapeDiverBackendErrors.js +13 -7
  32. package/dist/logger/ShapeDiverBackendErrors.js.map +1 -1
  33. package/dist/logger/ShapeDiverError.js +9 -4
  34. package/dist/logger/ShapeDiverError.js.map +1 -1
  35. package/dist/logger/ShapeDiverViewerErrors.js +40 -25
  36. package/dist/logger/ShapeDiverViewerErrors.js.map +1 -1
  37. package/dist/performance-evaluator/PerformanceEvaluator.d.ts +13 -0
  38. package/dist/performance-evaluator/PerformanceEvaluator.d.ts.map +1 -1
  39. package/dist/performance-evaluator/PerformanceEvaluator.js +39 -3
  40. package/dist/performance-evaluator/PerformanceEvaluator.js.map +1 -1
  41. package/dist/settings-engine/SettingsEngine.js +20 -16
  42. package/dist/settings-engine/SettingsEngine.js.map +1 -1
  43. package/dist/state-engine/StateEngine.js +8 -4
  44. package/dist/state-engine/StateEngine.js.map +1 -1
  45. package/dist/state-engine/StatePromise.js +5 -1
  46. package/dist/state-engine/StatePromise.js.map +1 -1
  47. package/dist/system-info/SystemInfo.js +10 -3
  48. package/dist/system-info/SystemInfo.js.map +1 -1
  49. package/dist/type-check/TypeChecker.js +5 -1
  50. package/dist/type-check/TypeChecker.js.map +1 -1
  51. package/dist/uuid-generator/UuidGenerator.js +10 -6
  52. package/dist/uuid-generator/UuidGenerator.js.map +1 -1
  53. package/package.json +4 -4
  54. package/src/converter/Converter.ts +69 -62
  55. package/src/http-client/HttpClient.ts +224 -31
  56. package/src/http-client/HttpResponse.ts +3 -1
  57. package/src/performance-evaluator/PerformanceEvaluator.ts +33 -3
  58. package/src/settings-engine/SettingsEngine.ts +1 -1
@@ -1,5 +1,5 @@
1
- import { vec3, vec4 } from 'gl-matrix'
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
- let svgC = document.createElement('DIV');
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
- let svgE = svgC.getElementsByTagName('svg');
41
- let childImageURIs: string[] = [];
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
- let childImages = <HTMLCollectionOf<SVGImageElement>>svgE[i].getElementsByTagName(['image', 'img'][j]);
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
- let styleElements = <HTMLCollectionOf<HTMLStyleElement>>svgE[i].getElementsByTagName('style');
57
+ const styleElements = <HTMLCollectionOf<HTMLStyleElement>>svgE[i].getElementsByTagName('style');
58
58
  for (let j = 0; j < styleElements.length; ++j) {
59
- let regex = /@import\x20url\(\s*(.*?)\s*\);/g;
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
- let replacementPromises = [];
75
- let createImagePromise = async (uri: string) => {
74
+ const replacementPromises = [];
75
+ const createImagePromise = async (uri: string) => {
76
76
  if (uri.length > 0) {
77
- const response = await this._httpClient.loadTexture(uri);
78
- let uInt8Array = new Uint8Array(response.data), i = uInt8Array.length;
79
- let biStr = []; //new Array(i);
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
- let base64Data = window.btoa(biStr.join(''));
84
- let imgDataUrl = 'data:' + response.headers['content-type'] + ';base64,' + base64Data;
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
- let createStylePromise = async (styleUrl: string) => {
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
- let fontURLs = [];
105
- let regex = /url\(\s*(.*?)\s*\)/g;
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
- let fontPromises = [];
112
- let createFontPromise = async (fUrl: string) => {
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
- let uInt8Array = new Uint8Array(response.data), i = uInt8Array.length;
118
- let biStr = []; //new Array(i);
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
- let base64Data = window.btoa(biStr.join(''));
123
- let fontDataUrl = 'data:' + response.headers['content-type'] + ';base64,' + base64Data;
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
- let du = 'data:image/svg+xml,' + encodeURIComponent(data);
141
- let img = new Image(); // same as document.createElement('img')
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 = "anonymous";
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: any): number {
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: any): number[] {
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: any, defColorString: string = '#000'): string {
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: any, defColorString: string = '#000'): string {
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
- let cl = cs.length;
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
- let tc = new TinyColor(cs);
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
- let tc = new TinyColor(color.getHexString());
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
- let tc = new TinyColor({
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
- let a = parseFloat(color[3]);
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
- let tc = new TinyColor(color);
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
- let tmpColor = color.replace('0x', '#');
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
- let tc = new TinyColor(tmpColor + 'ff');
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
- let tc = new TinyColor(tmpColor);
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
- let tc = new TinyColor(tmpColor.slice(0, 7) + '0' + tmpColor.slice(-1));
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
- let tc = new TinyColor(tmpColor);
292
+ const tc = new TinyColor(tmpColor);
288
293
  return tc.isValid ? this.tinyColorToString(tc) : defColorString;
289
294
  }
290
295
 
291
- public toThreeJsColorInput(color: any): string {
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: any): vec3 {
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
- if (((point.x || point.x === 0) && typeof point.x === 'number') && ((point.y || point.y === 0) && typeof point.y === 'number') && ((point.z || point.z === 0) && typeof point.z === 'number'))
301
- return vec3.fromValues(point.x, point.y, point.z);
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
- if (((point.X || point.X === 0) && typeof point.X === 'number') && ((point.Y || point.Y === 0) && typeof point.Y === 'number') && ((point.Z || point.Z === 0) && typeof point.Z === 'number'))
304
- return vec3.fromValues(point.X, point.Y, point.Z);
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 (2)
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 (2)
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
- public async get(href: string, config: AxiosRequestConfig = { responseType: 'arraybuffer' }, textureLoading: boolean = false): Promise<HttpResponse<any>> {
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
- return new Promise<HttpResponse<any>>((resolve, reject) => {
84
- sessionLoading!.downloadTexture(sessionId!, href).then((result) => {
85
- resolve({
86
- data: result[0],
87
- headers: {
88
- 'content-type': result[1]
89
- }
90
- })
91
- }).catch(e => reject(e))
92
- }).catch(e => { throw this.convertError(e) });
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
- return axios(href, Object.assign({ method: 'get' }, config))
97
- .catch(e => { throw this.convertError(e) });
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
- return axios(href, Object.assign({ method: 'get' }, config))
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
- return new Promise<HttpResponse<ArrayBuffer>>((resolve, reject) => {
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
- public async loadTexture(href: string): Promise<HttpResponse<ArrayBuffer>> {
128
- return this.get(href, undefined, true);
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 (1)
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
- // #endregion Private Methods (1)
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
  }
@@ -1,4 +1,6 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1
2
  export interface HttpResponse<T = any> {
2
3
  data: T;
3
- headers: any
4
+ headers: any,
5
+ size?: number
4
6
  }