@shapediver/viewer.shared.services 2.10.1-rc.0 → 2.12.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 (67) 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 +64 -55
  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.d.ts +7 -2
  11. package/dist/event-engine/EventTypes.d.ts.map +1 -1
  12. package/dist/event-engine/EventTypes.js +24 -15
  13. package/dist/event-engine/EventTypes.js.map +1 -1
  14. package/dist/event-engine/interfaces/ICallback.js +2 -1
  15. package/dist/event-engine/interfaces/IEvent.js +2 -1
  16. package/dist/event-engine/interfaces/IEvent.js.map +1 -1
  17. package/dist/event-engine/interfaces/IListener.js +2 -1
  18. package/dist/http-client/HttpClient.d.ts +78 -2
  19. package/dist/http-client/HttpClient.d.ts.map +1 -1
  20. package/dist/http-client/HttpClient.js +177 -20
  21. package/dist/http-client/HttpClient.js.map +1 -1
  22. package/dist/http-client/HttpResponse.d.ts +1 -0
  23. package/dist/http-client/HttpResponse.d.ts.map +1 -1
  24. package/dist/http-client/HttpResponse.js +2 -1
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +81 -32
  28. package/dist/index.js.map +1 -1
  29. package/dist/input-validator/InputValidator.js +16 -9
  30. package/dist/input-validator/InputValidator.js.map +1 -1
  31. package/dist/logger/ErrorTypeGuards.js +55 -35
  32. package/dist/logger/ErrorTypeGuards.js.map +1 -1
  33. package/dist/logger/Logger.js +7 -3
  34. package/dist/logger/Logger.js.map +1 -1
  35. package/dist/logger/ShapeDiverBackendErrors.js +13 -7
  36. package/dist/logger/ShapeDiverBackendErrors.js.map +1 -1
  37. package/dist/logger/ShapeDiverError.js +9 -4
  38. package/dist/logger/ShapeDiverError.js.map +1 -1
  39. package/dist/logger/ShapeDiverViewerErrors.js +40 -25
  40. package/dist/logger/ShapeDiverViewerErrors.js.map +1 -1
  41. package/dist/performance-evaluator/PerformanceEvaluator.d.ts +13 -0
  42. package/dist/performance-evaluator/PerformanceEvaluator.d.ts.map +1 -1
  43. package/dist/performance-evaluator/PerformanceEvaluator.js +39 -3
  44. package/dist/performance-evaluator/PerformanceEvaluator.js.map +1 -1
  45. package/dist/settings-engine/SettingsEngine.js +20 -16
  46. package/dist/settings-engine/SettingsEngine.js.map +1 -1
  47. package/dist/state-engine/StateEngine.d.ts +1 -0
  48. package/dist/state-engine/StateEngine.d.ts.map +1 -1
  49. package/dist/state-engine/StateEngine.js +8 -4
  50. package/dist/state-engine/StateEngine.js.map +1 -1
  51. package/dist/state-engine/StatePromise.js +5 -1
  52. package/dist/state-engine/StatePromise.js.map +1 -1
  53. package/dist/system-info/SystemInfo.js +10 -3
  54. package/dist/system-info/SystemInfo.js.map +1 -1
  55. package/dist/type-check/TypeChecker.js +5 -1
  56. package/dist/type-check/TypeChecker.js.map +1 -1
  57. package/dist/uuid-generator/UuidGenerator.js +10 -6
  58. package/dist/uuid-generator/UuidGenerator.js.map +1 -1
  59. package/package.json +4 -4
  60. package/src/converter/Converter.ts +69 -64
  61. package/src/event-engine/EventTypes.ts +8 -2
  62. package/src/http-client/HttpClient.ts +204 -26
  63. package/src/http-client/HttpResponse.ts +3 -1
  64. package/src/index.ts +2 -2
  65. package/src/performance-evaluator/PerformanceEvaluator.ts +33 -3
  66. package/src/settings-engine/SettingsEngine.ts +1 -1
  67. package/src/state-engine/StateEngine.ts +2 -0
@@ -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,21 +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> {
153
- const arrayBufferView = new Uint8Array( response.data );
154
- const blob = new Blob([ arrayBufferView ], { type: response.headers['content-type'] } );
154
+ public async responseToImage(response: HttpResponse<ArrayBuffer | HTMLImageElement>, blob: Blob): Promise<HTMLImageElement> {
155
+ // if we already receive and image, this conversion already happened
156
+ if(response.data instanceof HTMLImageElement) return response.data;
157
+
155
158
  if (response.headers['content-type'] === 'image/svg+xml') {
156
159
  const img = await this.processSVG(blob);
157
160
  return img;
@@ -160,8 +163,8 @@ export class Converter {
160
163
  const promise = new Promise<void>((resolve, reject) => {
161
164
  img.onload = () => resolve();
162
165
  img.onerror = reject;
163
- })
164
- img.crossOrigin = "anonymous";
166
+ });
167
+ img.crossOrigin = 'anonymous';
165
168
  img.src = URL.createObjectURL(blob);
166
169
  await promise;
167
170
  URL.revokeObjectURL(img.src);
@@ -169,17 +172,17 @@ export class Converter {
169
172
  }
170
173
  }
171
174
 
172
- public toAlpha(color: any): number {
175
+ public toAlpha(color: unknown): number {
173
176
  const c = this.toHexColor(color);
174
177
  if (c.length <= 8) return 1;
175
178
  return parseInt(c.slice(c.length - 2, c.length), 16) / 255;
176
179
  }
177
180
 
178
- public toColorArray(color: any): number[] {
179
- if(typeof color !== 'string' || !color.startsWith("#"))
181
+ public toColorArray(color: unknown): number[] {
182
+ if(typeof color !== 'string' || !color.startsWith('#'))
180
183
  color = this.toHexColor(color);
181
- const tColor = new TinyColor(color);
182
- const rgb = tColor.toRgb()
184
+ const tColor = new TinyColor(color as ColorInput | undefined);
185
+ const rgb = tColor.toRgb();
183
186
  return [rgb.r / 255.0, rgb.g / 255.0, rgb.b / 255.0];
184
187
  }
185
188
 
@@ -187,7 +190,7 @@ export class Converter {
187
190
  * @param color
188
191
  * @param defColor
189
192
  */
190
- public toHex8Color(color: any, defColorString: string = '#000'): string {
193
+ public toHex8Color(color: unknown, defColorString: string = '#000'): string {
191
194
  const c = this.toHexColor(color, defColorString);
192
195
  const tColor = new TinyColor(c);
193
196
  const cH8 = tColor.toHex8String();
@@ -201,13 +204,13 @@ export class Converter {
201
204
  * @param color
202
205
  * @param defColor
203
206
  */
204
- public toHexColor(color: any, defColorString: string = '#000'): string {
207
+ public toHexColor(color: unknown, defColorString: string = '#000'): string {
205
208
  if (!color || color === 'default') return defColorString;
206
209
 
207
- if (color.constructor === Float32Array)
208
- color = Array.from(color);
210
+ if ((color as Float32Array).constructor === Float32Array)
211
+ color = Array.from(color as Float32Array);
209
212
 
210
- const tColor = new TinyColor(color);
213
+ const tColor = new TinyColor(color as TinyColor | undefined);
211
214
 
212
215
  if (color instanceof TinyColor)
213
216
  return this.tinyColorToString(tColor);
@@ -215,17 +218,17 @@ export class Converter {
215
218
  // check if we got a number
216
219
  if (typeof color === 'number') {
217
220
  let cs = color.toString(16);
218
- let cl = cs.length;
221
+ const cl = cs.length;
219
222
  if (cl < 3) cs = cs.padStart(3, '0');
220
223
  else if (cl < 6) cs = cs.padStart(6, '0');
221
224
  else if (cl < 8) cs = cs.padEnd(8, '0');
222
- let tc = new TinyColor(cs);
225
+ const tc = new TinyColor(cs);
223
226
  return tc.isValid ? this.tinyColorToString(tc) : defColorString;
224
227
  }
225
228
 
226
229
  // check if the input is a THREE.Color
227
- if (color.isColor && typeof color.getHexString == 'function') {
228
- let tc = new TinyColor(color.getHexString());
230
+ if ((color as THREE.Color).isColor && typeof (color as THREE.Color).getHexString == 'function') {
231
+ const tc = new TinyColor((color as THREE.Color).getHexString());
229
232
  return tc.isValid ? this.tinyColorToString(tc) : defColorString;
230
233
  }
231
234
 
@@ -241,13 +244,13 @@ export class Converter {
241
244
  if (!isRGBArray)
242
245
  return defColorString;
243
246
 
244
- let tc = new TinyColor({
247
+ const tc = new TinyColor({
245
248
  r: Math.max(0, Math.min(color[0], 255)),
246
249
  g: Math.max(0, Math.min(color[1], 255)),
247
250
  b: Math.max(0, Math.min(color[2], 255))
248
251
  });
249
252
  if (color.length == 4) {
250
- let a = parseFloat(color[3]);
253
+ const a = parseFloat(color[3]);
251
254
  if (!isNaN(a)) {
252
255
  tc.setAlpha(Math.max(0, Math.min(a, 255)) / 255);
253
256
  }
@@ -258,50 +261,52 @@ export class Converter {
258
261
  // if we got something other than a string, check if
259
262
  // tinycolor can work with it
260
263
  if (typeof color !== 'string') {
261
- let tc = new TinyColor(color);
264
+ const tc = new TinyColor(color as string);
262
265
  return tc.isValid ? this.tinyColorToString(tc) : defColorString;
263
266
  }
264
267
 
265
268
  // tinycolor doesn't like 0x
266
- let tmpColor = color.replace('0x', '#');
269
+ const tmpColor = color.replace('0x', '#');
267
270
 
268
271
  // if we got no alpha value, add full opacity
269
272
  if (tmpColor.match(/^#[a-f0-9]{6}$/i) !== null) {
270
- let tc = new TinyColor(tmpColor + 'ff');
273
+ const tc = new TinyColor(tmpColor + 'ff');
271
274
  return tc.isValid ? this.tinyColorToString(tc) : defColorString;
272
275
  }
273
276
 
274
277
  // standard case
275
278
  if (tmpColor.match(/^#[a-f0-9]{8}$/i) !== null) {
276
- let tc = new TinyColor(tmpColor);
279
+ const tc = new TinyColor(tmpColor);
277
280
  return tc.isValid ? this.tinyColorToString(tc) : defColorString;
278
281
  }
279
282
 
280
283
  // correct number which have the alpha value defined as a single hex digit
281
284
  if (tmpColor.match(/^#[a-f0-9]{7}$/i) !== null) {
282
- let tc = new TinyColor(tmpColor.slice(0, 7) + '0' + tmpColor.slice(-1));
285
+ const tc = new TinyColor(tmpColor.slice(0, 7) + '0' + tmpColor.slice(-1));
283
286
  return tc.isValid ? this.tinyColorToString(tc) : defColorString;
284
287
  }
285
288
 
286
289
  // check if tinycolor understands the string
287
- let tc = new TinyColor(tmpColor);
290
+ const tc = new TinyColor(tmpColor);
288
291
  return tc.isValid ? this.tinyColorToString(tc) : defColorString;
289
292
  }
290
293
 
291
- public toThreeJsColorInput(color: any): string {
294
+ public toThreeJsColorInput(color: unknown): string {
292
295
  const c = this.toHexColor(color);
293
296
  return c.slice(0, c.length - 2);
294
297
  }
295
298
 
296
- public toVec3(point: any): vec3 {
299
+ public toVec3(point: vec3 | { x: number, y: number, z: number} | { X: number, Y: number, Z: number}): vec3 {
297
300
  if (Array.isArray(point) && point.length >= 3 && typeof point[0] === 'number' && typeof point[1] === 'number' && typeof point[2] === 'number')
298
301
  return vec3.fromValues(point[0], point[1], point[2]);
299
302
 
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);
303
+ const pointCast1 = point as { x: number, y: number, z: number};
304
+ 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'))
305
+ return vec3.fromValues(pointCast1.x, pointCast1.y, pointCast1.z);
302
306
 
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);
307
+ const pointCast2 = point as { X: number, Y: number, Z: number};
308
+ 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'))
309
+ return vec3.fromValues(pointCast2.X, pointCast2.Y, pointCast2.Z);
305
310
 
306
311
  return vec3.create();
307
312
  }
@@ -9,7 +9,8 @@ export enum EVENTTYPE_RENDERING {
9
9
  }
10
10
 
11
11
  export enum EVENTTYPE_SCENE {
12
- SCENE_BOUNDING_BOX_CHANGE = "scene.boundingBoxChange"
12
+ SCENE_BOUNDING_BOX_CHANGE = "scene.boundingBoxChange",
13
+ SCENE_BOUNDING_BOX_EMPTY = "scene.boundingBoxEmpty"
13
14
  }
14
15
 
15
16
  // VIEWPORT
@@ -39,6 +40,10 @@ export enum EVENTTYPE_SESSION {
39
40
  SESSION_INITIAL_OUTPUTS_LOADED = 'session.initialOutputsLoaded'
40
41
  }
41
42
 
43
+ export enum EVENTTYPE_OUTPUT {
44
+ OUTPUT_UPDATED = "output.updated"
45
+ }
46
+
42
47
  export enum EVENTTYPE_TASK {
43
48
  TASK_START = "task.start",
44
49
  TASK_PROCESS = "task.process",
@@ -58,6 +63,7 @@ export enum EVENTTYPE_INTERACTION {
58
63
 
59
64
  export const EVENTTYPE = {
60
65
  CAMERA: EVENTTYPE_CAMERA,
66
+ OUTPUT: EVENTTYPE_OUTPUT,
61
67
  RENDERING: EVENTTYPE_RENDERING,
62
68
  SCENE: EVENTTYPE_SCENE,
63
69
  SESSION: EVENTTYPE_SESSION,
@@ -66,4 +72,4 @@ export const EVENTTYPE = {
66
72
  TASK: EVENTTYPE_TASK
67
73
  };
68
74
 
69
- export type MainEventTypes = typeof EVENTTYPE_CAMERA | typeof EVENTTYPE_RENDERING | typeof EVENTTYPE_SCENE | typeof EVENTTYPE_SESSION | typeof EVENTTYPE_VIEWPORT | typeof EVENTTYPE_INTERACTION | typeof EVENTTYPE_TASK;
75
+ export type MainEventTypes = typeof EVENTTYPE_CAMERA | typeof EVENTTYPE_OUTPUT | typeof EVENTTYPE_RENDERING | typeof EVENTTYPE_SCENE | typeof EVENTTYPE_SESSION | typeof EVENTTYPE_VIEWPORT | typeof EVENTTYPE_INTERACTION | typeof EVENTTYPE_TASK;
@@ -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): 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,40 @@ 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) => {
150
+ loadingPromise = new Promise<HttpResponse<ArrayBuffer>>((resolve, reject) => {
151
+ sessionLoading!.downloadTexture(sessionId!, href).then(async (result) => {
85
152
  resolve({
86
153
  data: result[0],
87
154
  headers: {
88
155
  'content-type': result[1]
89
156
  }
90
- })
91
- }).catch(e => reject(e))
92
- }).catch(e => { throw this.convertError(e) });
157
+ });
158
+ }).catch(e => reject(e));
159
+ }).catch(e => { throw this.convertError(e); });
93
160
  } else {
94
161
  // we can load blobs and data urls directly
95
162
  // 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) });
163
+ loadingPromise = axios(href, Object.assign({ method: 'get' }, config))
164
+ .then(async (result) => {
165
+ return result;
166
+ })
167
+ .catch(e => { throw this.convertError(e); });
98
168
  }
99
169
  } else {
100
170
  if (!sessionLoading) {
101
171
  // 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) });
172
+ loadingPromise = axios(href, Object.assign({ method: 'get' }, config))
173
+ .catch(e => { throw this.convertError(e); });
104
174
  } else {
105
175
  // all data links where we could somehow find a session to load it with
106
- return new Promise<HttpResponse<ArrayBuffer>>((resolve, reject) => {
176
+ loadingPromise = new Promise<HttpResponse<ArrayBuffer>>((resolve, reject) => {
107
177
  sessionLoading!.getAsset(href)
108
178
  .then((result) => {
109
179
  resolve({
@@ -111,31 +181,115 @@ export class HttpClient {
111
181
  headers: {
112
182
  'content-type': result[1]
113
183
  }
114
- })
184
+ });
115
185
  })
116
186
  .catch(() => {
117
187
  // if this fails, we just load it directly
118
188
  const axiosPromise = axios(href, Object.assign({ method: 'get' }, config));
119
- axiosPromise.catch(e => reject(e))
189
+ axiosPromise.catch(e => reject(e));
120
190
  resolve(axiosPromise);
121
191
  });
122
- }).catch(e => { throw this.convertError(e) });
192
+ }).catch(e => { throw this.convertError(e); });
123
193
  }
124
194
  }
195
+
196
+ if (this.enableCaching)
197
+ this.addToCache(dataKey, loadingPromise);
198
+
199
+ return loadingPromise;
125
200
  }
126
201
 
127
- public async loadTexture(href: string): Promise<HttpResponse<ArrayBuffer>> {
128
- return this.get(href, undefined, true);
202
+ /**
203
+ * Get the requested texture either as a download or from the cache.
204
+ *
205
+ * @param href
206
+ * @returns
207
+ */
208
+ public async loadTexture(href: string): Promise<HttpResponse<{ image: HTMLImageElement, blob: Blob }>> {
209
+ const response = await (this.get(href, undefined, true) as Promise<HttpResponse<ArrayBuffer>>);
210
+ const arrayBufferView = new Uint8Array( response.data );
211
+ const blob = new Blob([ arrayBufferView ], { type: response.headers['content-type'] } );
212
+ const image = await Converter.instance.responseToImage(response, blob);
213
+ return {
214
+ data: {
215
+ image,
216
+ blob
217
+ },
218
+ size: response.data.byteLength,
219
+ headers: response.headers
220
+ };
129
221
  }
130
222
 
223
+ /**
224
+ * Add the data loading options from a session.
225
+ *
226
+ * @param sessionId
227
+ */
131
228
  public removeDataLoading(sessionId: string) {
132
229
  delete this._sessionLoading[sessionId];
133
230
  }
134
231
 
135
232
  // #endregion Public Methods (5)
136
233
 
137
- // #region Private Methods (1)
234
+ // #region Private Methods (5)
235
+
236
+ /**
237
+ * Add
238
+ *
239
+ * @param key
240
+ * @param value
241
+ */
242
+ private addToCache(key: string, value: Promise<HttpResponse<unknown>>) {
243
+ // Remove items from the cache until the cache size is smaller than the maximum cache size.
244
+ // Only resolved promises are evaluated, as unresolved promises don't add any size.
245
+ while (this.calculateCacheSize() >= this._maxCacheSize) {
246
+ // Remove the oldest entry if the cache is full
247
+ const oldestKey = this._dataCache.keys().next().value;
248
+ this._dataCache.delete(oldestKey);
249
+ }
250
+
251
+ const timestamp = Date.now();
252
+ this._dataCache.set(key, { value, timestamp, resolved: false });
253
+
254
+ // once the promise resolves, set resolved and size properties
255
+ value.then((promiseResult) => {
256
+ const size = promiseResult.size ? promiseResult.size : (promiseResult.data as ArrayBuffer).byteLength;
257
+ this._dataCache.set(key, { value, timestamp, resolved: true, size });
258
+ }).catch(e => { throw this.convertError(e); });
259
+ }
260
+
261
+ /**
262
+ * Calculate the current cache size from all resolved promises.
263
+ *
264
+ * @returns
265
+ */
266
+ private calculateCacheSize() {
267
+ let size = 0;
268
+ this._dataCache.forEach(value => {
269
+ if (value.resolved === true)
270
+ size += value.size!;
271
+ });
272
+ return size;
273
+ }
274
+
275
+ /**
276
+ * Get the value of an object from the cache.
277
+ *
278
+ * @param key
279
+ * @returns
280
+ */
281
+ private getFromCache(key: string): Promise<HttpResponse<unknown>> {
282
+ const cachedObject = this._dataCache.get(key)!;
283
+ this._dataCache.set(key, { value: cachedObject.value, timestamp: Date.now(), resolved: cachedObject.resolved, size: cachedObject.size });
284
+ return cachedObject.value;
285
+ }
138
286
 
287
+ /**
288
+ * Get the session id of the provided href.
289
+ *
290
+ * @param href
291
+ * @returns
292
+ */
139
293
  private getSessionId(href: string): string | undefined {
140
294
  // searching for "/session/SESSION_ID/{'output' | 'export' | 'texture'}/ASSET_DATA"
141
295
  const parts = href.split('/');
@@ -151,5 +305,29 @@ export class HttpClient {
151
305
  return;
152
306
  }
153
307
 
154
- // #endregion Private Methods (1)
308
+ /**
309
+ * Convert the provided href to a data cache key.
310
+ * In this conversion the excludedQueryParameters are removed from the href.
311
+ *
312
+ * @param href
313
+ * @returns
314
+ */
315
+ private hrefToDataKey(href: string) {
316
+ const url = new URL(href);
317
+
318
+ // Create a URLSearchParams object from the existing query parameters
319
+ const params = new URLSearchParams(url.search);
320
+
321
+ for (let i = 0; i < this._excludedQueryParameters.length; i++)
322
+ // Remove specific query parameters
323
+ params.delete(this._excludedQueryParameters[i]);
324
+
325
+ // Reconstruct the URL with the modified query parameters
326
+ url.search = params.toString();
327
+
328
+ const hrefAsKey = url.toString();
329
+ return window.btoa(hrefAsKey);
330
+ }
331
+
332
+ // #endregion Private Methods (5)
155
333
  }