@smarterplan/ngx-smarterplan-core 1.4.15 → 1.4.16

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 (102) hide show
  1. package/README.md +24 -24
  2. package/esm2022/lib/components/csv-export/csv-export.component.mjs +3 -3
  3. package/esm2022/lib/components/loader/loader.component.mjs +3 -3
  4. package/esm2022/lib/components/menu-bar/avatar/avatar.component.mjs +1 -1
  5. package/esm2022/lib/components/menu-bar/menu-bar.component.mjs +3 -3
  6. package/esm2022/lib/components/menu-bar/navigation-bar/navigation-bar.component.mjs +3 -3
  7. package/esm2022/lib/components/modal-switch-visit/modal-switch-visit.component.mjs +3 -3
  8. package/esm2022/lib/components/search-bar/search-bar.component.mjs +3 -3
  9. package/esm2022/lib/components/support-modal/support-modal.component.mjs +3 -3
  10. package/esm2022/lib/config.mjs +1 -1
  11. package/esm2022/lib/helpers.service.mjs +46 -1
  12. package/esm2022/lib/matterport-extensions/nest-thermostat/CanvasImage.mjs +1 -1
  13. package/esm2022/lib/matterport-extensions/nest-thermostat/CanvasRenderer.mjs +1 -1
  14. package/esm2022/lib/matterport-extensions/nest-thermostat/NestThermostat.mjs +1 -1
  15. package/esm2022/lib/matterport-extensions/nest-thermostat/PlaneRenderer.mjs +1 -1
  16. package/esm2022/lib/matterport-extensions/scene-component/SceneComponent.mjs +1 -1
  17. package/esm2022/lib/matterport-extensions/security-camera/SecurityCamera.mjs +22 -22
  18. package/esm2022/lib/matterport-extensions/tv-player/TvPlayer.mjs +1 -1
  19. package/esm2022/lib/matterport-extensions/view-frustum-mesh/ViewFrustumMesh.mjs +22 -22
  20. package/esm2022/lib/mattertagData.mjs +1 -1
  21. package/esm2022/lib/ngx-smarterplan-core.module.mjs +1 -1
  22. package/esm2022/lib/ngx-smarterplan-core.service.mjs +1 -1
  23. package/esm2022/lib/pipes/duration-to-string.pipe.mjs +1 -1
  24. package/esm2022/lib/pipes/format-date-number-to-digits.pipe.mjs +1 -1
  25. package/esm2022/lib/pipes/hashtag-from-id.pipe.mjs +1 -1
  26. package/esm2022/lib/pipes/safe-url.pipe.mjs +1 -1
  27. package/esm2022/lib/pipes/time-date-to-local-string.pipe.mjs +1 -1
  28. package/esm2022/lib/pipes/username-from-id.pipe.mjs +1 -1
  29. package/esm2022/lib/services/amplify-cache.service.mjs +1 -1
  30. package/esm2022/lib/services/base-tab.service.mjs +1 -1
  31. package/esm2022/lib/services/baseVisibility.service.mjs +1 -1
  32. package/esm2022/lib/services/content.service.mjs +1 -1
  33. package/esm2022/lib/services/filter.service.mjs +1 -1
  34. package/esm2022/lib/services/intervention.service.mjs +1 -1
  35. package/esm2022/lib/services/locale.service.mjs +1 -1
  36. package/esm2022/lib/services/matterport-import.service.mjs +1 -1
  37. package/esm2022/lib/services/matterport-measurement.service.mjs +1 -1
  38. package/esm2022/lib/services/matterport-navigation.service.mjs +1 -1
  39. package/esm2022/lib/services/matterport-object3d.service.mjs +1 -1
  40. package/esm2022/lib/services/matterport-pointer.service.mjs +6 -6
  41. package/esm2022/lib/services/matterport-tag-copy.service.mjs +1 -1
  42. package/esm2022/lib/services/matterport-tag.service.mjs +1 -1
  43. package/esm2022/lib/services/matterport.service.mjs +17 -17
  44. package/esm2022/lib/services/models/affectation.service.mjs +1 -1
  45. package/esm2022/lib/services/models/base-object.service.mjs +1 -1
  46. package/esm2022/lib/services/models/capture.service.mjs +1 -1
  47. package/esm2022/lib/services/models/comment.service.mjs +1 -1
  48. package/esm2022/lib/services/models/domain.service.mjs +1 -1
  49. package/esm2022/lib/services/models/equipment.service.mjs +1 -1
  50. package/esm2022/lib/services/models/event.service.mjs +1 -1
  51. package/esm2022/lib/services/models/feature.service.mjs +1 -1
  52. package/esm2022/lib/services/models/hashtag.service.mjs +1 -1
  53. package/esm2022/lib/services/models/layer.service.mjs +1 -1
  54. package/esm2022/lib/services/models/measurement.service.mjs +1 -1
  55. package/esm2022/lib/services/models/mission.service.mjs +1 -1
  56. package/esm2022/lib/services/models/navigation.service.mjs +1 -1
  57. package/esm2022/lib/services/models/node.service.mjs +1 -1
  58. package/esm2022/lib/services/models/object3D.service.mjs +1 -1
  59. package/esm2022/lib/services/models/operation.service.mjs +1 -1
  60. package/esm2022/lib/services/models/organisation.service.mjs +1 -1
  61. package/esm2022/lib/services/models/plan.service.mjs +1 -1
  62. package/esm2022/lib/services/models/poi.service.mjs +27 -27
  63. package/esm2022/lib/services/models/profile.service.mjs +1 -1
  64. package/esm2022/lib/services/models/property.service.mjs +1 -1
  65. package/esm2022/lib/services/models/space.service.mjs +25 -6
  66. package/esm2022/lib/services/models/template.service.mjs +1 -1
  67. package/esm2022/lib/services/models/ticket.service.mjs +1 -1
  68. package/esm2022/lib/services/models/visit.service.mjs +17 -5
  69. package/esm2022/lib/services/models/zone.service.mjs +1 -1
  70. package/esm2022/lib/services/navigator.service.mjs +1 -1
  71. package/esm2022/lib/services/s3.service.mjs +18 -1
  72. package/esm2022/lib/services/search.service.mjs +1 -1
  73. package/esm2022/lib/services/support.service.mjs +1 -1
  74. package/esm2022/lib/services/tag.service.mjs +16 -16
  75. package/esm2022/lib/services/threejs-import.service.mjs +151 -0
  76. package/esm2022/lib/services/threejs.service.mjs +1306 -0
  77. package/esm2022/lib/services/user.service.mjs +1 -1
  78. package/esm2022/lib/services/utils/matterport-transform.util.mjs +1 -1
  79. package/esm2022/lib/services/validators.service.mjs +1 -1
  80. package/esm2022/lib/services/viewer.service.mjs +263 -5
  81. package/esm2022/lib/services/zone-drawer.service.mjs +1 -1
  82. package/esm2022/lib/services/zoneChange.service.mjs +1 -1
  83. package/esm2022/lib/tokens/matterport-sdk-key.token.mjs +1 -1
  84. package/esm2022/lib/types.service.mjs +2 -1
  85. package/esm2022/lib/validators/email.directive.mjs +1 -1
  86. package/esm2022/lib/validators/no-empty.directive.mjs +1 -1
  87. package/esm2022/lib/validators/number.directive.mjs +1 -1
  88. package/esm2022/lib/validators/text.directive.mjs +1 -1
  89. package/esm2022/public-api.mjs +3 -1
  90. package/fesm2022/smarterplan-ngx-smarterplan-core.mjs +2196 -402
  91. package/fesm2022/smarterplan-ngx-smarterplan-core.mjs.map +1 -1
  92. package/lib/helpers.service.d.ts +43 -0
  93. package/lib/services/models/space.service.d.ts +15 -1
  94. package/lib/services/models/visit.service.d.ts +9 -1
  95. package/lib/services/s3.service.d.ts +8 -0
  96. package/lib/services/threejs-import.service.d.ts +31 -0
  97. package/lib/services/threejs.service.d.ts +423 -0
  98. package/lib/services/viewer.service.d.ts +114 -1
  99. package/lib/types.service.d.ts +7 -1
  100. package/package.json +1 -1
  101. package/public-api.d.ts +2 -0
  102. package/src/assets/vs-app.json +67 -67
@@ -7,12 +7,19 @@ import * as i1$2 from '@ng-bootstrap/ng-bootstrap';
7
7
  import { NgbDate, NgbModule } from '@ng-bootstrap/ng-bootstrap';
8
8
  import * as i1$1 from '@angular/router';
9
9
  import { RouterModule } from '@angular/router';
10
+ import * as THREE from 'three';
10
11
  import { Matrix4, Vector3, Color, Object3D, Euler, PerspectiveCamera, CylinderBufferGeometry, BoxGeometry, MeshBasicMaterial, NormalBlending, BackSide, Mesh, EdgesGeometry, AdditiveBlending, ShaderMaterial, AnimationMixer, VectorKeyframeTrack, InterpolateSmooth, AnimationClip, LoopOnce, Group, PlaneBufferGeometry, CanvasTexture, Texture, LinearFilter } from 'three';
11
12
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
12
- import { Auth, API, graphqlOperation, Storage, Cache } from 'aws-amplify';
13
+ import { Storage, Auth, API, graphqlOperation, Cache } from 'aws-amplify';
13
14
  import { DateTime } from 'luxon';
14
15
  import * as i11 from '@angular/common';
15
16
  import { CommonModule } from '@angular/common';
17
+ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
18
+ import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
19
+ import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';
20
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
21
+ import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader';
22
+ import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
16
23
  import * as FileSaver from 'file-saver';
17
24
  import levenshtein from 'js-levenshtein';
18
25
  import InformationIcon from '@iconify/icons-mdi/information-outline';
@@ -29,6 +36,7 @@ import * as CryptoJS from 'crypto-js';
29
36
  import { GlobalWorkerOptions, getDocument } from 'pdfjs-dist';
30
37
  import panzoom from 'panzoom';
31
38
  import videojs from 'video.js';
39
+ import { v4 } from 'uuid';
32
40
 
33
41
  class NgxSmarterplanCoreService {
34
42
  constructor() { }
@@ -79,11 +87,11 @@ class CsvExportComponent {
79
87
  link.remove();
80
88
  }
81
89
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CsvExportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
82
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: CsvExportComponent, selector: "lib-csv-export", inputs: { headers: "headers", rowsInput: "rowsInput", getRows: "getRows", title: "title", buttonLabel: "buttonLabel" }, ngImport: i0, template: "<span [style.cursor]=\"'pointer'\" (click)=\"onClickDownload()\" class=\"download-csv d-flex\">\r\n <span class=\"iconify\" data-icon=\"mdi:file-download-outline\"></span>\r\n {{ buttonLabel | translate}}\r\n</span>\r\n", styles: [".download-csv{color:var(--smarterplan-primary)}\n"], dependencies: [{ kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
90
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: CsvExportComponent, selector: "lib-csv-export", inputs: { headers: "headers", rowsInput: "rowsInput", getRows: "getRows", title: "title", buttonLabel: "buttonLabel" }, ngImport: i0, template: "<span [style.cursor]=\"'pointer'\" (click)=\"onClickDownload()\" class=\"download-csv d-flex\">\n <span class=\"iconify\" data-icon=\"mdi:file-download-outline\"></span>\n {{ buttonLabel | translate}}\n</span>\n", styles: [".download-csv{color:var(--smarterplan-primary)}\n"], dependencies: [{ kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
83
91
  }
84
92
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CsvExportComponent, decorators: [{
85
93
  type: Component,
86
- args: [{ selector: 'lib-csv-export', template: "<span [style.cursor]=\"'pointer'\" (click)=\"onClickDownload()\" class=\"download-csv d-flex\">\r\n <span class=\"iconify\" data-icon=\"mdi:file-download-outline\"></span>\r\n {{ buttonLabel | translate}}\r\n</span>\r\n", styles: [".download-csv{color:var(--smarterplan-primary)}\n"] }]
94
+ args: [{ selector: 'lib-csv-export', template: "<span [style.cursor]=\"'pointer'\" (click)=\"onClickDownload()\" class=\"download-csv d-flex\">\n <span class=\"iconify\" data-icon=\"mdi:file-download-outline\"></span>\n {{ buttonLabel | translate}}\n</span>\n", styles: [".download-csv{color:var(--smarterplan-primary)}\n"] }]
87
95
  }], propDecorators: { headers: [{
88
96
  type: Input
89
97
  }], rowsInput: [{
@@ -248,6 +256,7 @@ var CaptureViewer;
248
256
  CaptureViewer["CUPIX"] = "CUPIX";
249
257
  CaptureViewer["MATTERPORT"] = "MATTERPORT";
250
258
  CaptureViewer["SMARTERPLAN_V1"] = "SMARTERPLAN_V1";
259
+ CaptureViewer["THREEJS"] = "THREEJS";
251
260
  })(CaptureViewer || (CaptureViewer = {}));
252
261
  var InventoryStatus;
253
262
  (function (InventoryStatus) {
@@ -786,30 +795,30 @@ class SecurityCamera extends SceneComponent {
786
795
  }
787
796
  }
788
797
  function vertexShader$1() {
789
- return `
790
- uniform mat4 lightMatrix;
791
-
792
- varying vec4 vLightCoords;
793
-
794
- void main() {
795
- // create a UV region [0,1] x [0,1] to represent what the lightMatrix "sees"
796
- vLightCoords = lightMatrix * modelMatrix * vec4(position, 1.0);
797
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
798
- }
798
+ return `
799
+ uniform mat4 lightMatrix;
800
+
801
+ varying vec4 vLightCoords;
802
+
803
+ void main() {
804
+ // create a UV region [0,1] x [0,1] to represent what the lightMatrix "sees"
805
+ vLightCoords = lightMatrix * modelMatrix * vec4(position, 1.0);
806
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
807
+ }
799
808
  `;
800
809
  }
801
810
  function fragmentShader$1() {
802
- return `
803
- uniform vec3 color;
804
-
805
- varying vec4 vLightCoords;
806
-
807
- void main() {
808
- vec2 lightUV = vLightCoords.xy / vLightCoords.w;
809
- float inView = float(max(lightUV.x, lightUV.y) <= 1.0 && min(lightUV.x, lightUV.y) >= 0.0 && vLightCoords.z > 0.0);
810
-
811
- gl_FragColor = vec4(color, min(inView, 0.2));
812
- }
811
+ return `
812
+ uniform vec3 color;
813
+
814
+ varying vec4 vLightCoords;
815
+
816
+ void main() {
817
+ vec2 lightUV = vLightCoords.xy / vLightCoords.w;
818
+ float inView = float(max(lightUV.x, lightUV.y) <= 1.0 && min(lightUV.x, lightUV.y) >= 0.0 && vLightCoords.z > 0.0);
819
+
820
+ gl_FragColor = vec4(color, min(inView, 0.2));
821
+ }
813
822
  `;
814
823
  }
815
824
  function updateHighlightUniforms$1(projector, uniforms) {
@@ -1002,30 +1011,30 @@ class ViewFrustumMesh extends SceneComponent {
1002
1011
  }
1003
1012
  }
1004
1013
  function vertexShader() {
1005
- return `
1006
- uniform mat4 lightMatrix;
1007
-
1008
- varying vec4 vLightCoords;
1009
-
1010
- void main() {
1011
- // create a UV region [0,1] x [0,1] to represent what the lightMatrix "sees"
1012
- vLightCoords = lightMatrix * modelMatrix * vec4(position, 1.0);
1013
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
1014
- }
1014
+ return `
1015
+ uniform mat4 lightMatrix;
1016
+
1017
+ varying vec4 vLightCoords;
1018
+
1019
+ void main() {
1020
+ // create a UV region [0,1] x [0,1] to represent what the lightMatrix "sees"
1021
+ vLightCoords = lightMatrix * modelMatrix * vec4(position, 1.0);
1022
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
1023
+ }
1015
1024
  `;
1016
1025
  }
1017
1026
  function fragmentShader() {
1018
- return `
1019
- uniform vec3 color;
1020
-
1021
- varying vec4 vLightCoords;
1022
-
1023
- void main() {
1024
- vec2 lightUV = vLightCoords.xy / vLightCoords.w;
1025
- float inView = float(max(lightUV.x, lightUV.y) <= 1.0 && min(lightUV.x, lightUV.y) >= 0.0 && vLightCoords.z > 0.0);
1026
-
1027
- gl_FragColor = vec4(color, min(inView, 0.2));
1028
- }
1027
+ return `
1028
+ uniform vec3 color;
1029
+
1030
+ varying vec4 vLightCoords;
1031
+
1032
+ void main() {
1033
+ vec2 lightUV = vLightCoords.xy / vLightCoords.w;
1034
+ float inView = float(max(lightUV.x, lightUV.y) <= 1.0 && min(lightUV.x, lightUV.y) >= 0.0 && vLightCoords.z > 0.0);
1035
+
1036
+ gl_FragColor = vec4(color, min(inView, 0.2));
1037
+ }
1029
1038
  `;
1030
1039
  }
1031
1040
  function updateHighlightUniforms(projector, uniforms) {
@@ -1042,6 +1051,198 @@ const makeViewFrustumMesh = function () {
1042
1051
  return new ViewFrustumMesh();
1043
1052
  };
1044
1053
 
1054
+ const deleteFromS3 = async (path) => {
1055
+ await Storage.remove(path, { level: "public" })
1056
+ .then((result) => console.log("Successfully deleted =>", result))
1057
+ .catch((error) => console.log("Deleting from S3 error:", error));
1058
+ };
1059
+ const uploadFileToS3 = async (path, file, objectId) => {
1060
+ const prefix = objectId ? `${objectId.slice(0, 8)}_` : "";
1061
+ return Storage.put(path + prefix + file.name, file, {
1062
+ progressCallback(progress) {
1063
+ // console.log('Uploaded : ', progress);
1064
+ },
1065
+ contentType: file.type,
1066
+ })
1067
+ .then((result) => {
1068
+ console.log("Success =>", result);
1069
+ return result.key;
1070
+ });
1071
+ };
1072
+ /**
1073
+ * Uploads a file, reporting progress as a 0-1 fraction.
1074
+ *
1075
+ * uploadFileToS3 above swallows progress, which is fine for a photo and unusable for a 3D model:
1076
+ * these run to hundreds of megabytes, and an upload with no feedback is indistinguishable from a
1077
+ * frozen page. Returns the stored key, which is what gets saved on the record.
1078
+ */
1079
+ const uploadFileToS3WithProgress = async (path, file, onProgress) => {
1080
+ return Storage.put(path + file.name, file, {
1081
+ contentType: file.type || "application/octet-stream",
1082
+ progressCallback(progress) {
1083
+ if (onProgress && progress.total > 0) {
1084
+ onProgress(progress.loaded / progress.total);
1085
+ }
1086
+ },
1087
+ }).then((result) => result.key);
1088
+ };
1089
+ const getSignedImageUrlForEquipment = async (equip) => {
1090
+ if (!equip.photo) {
1091
+ return Promise.resolve(null);
1092
+ }
1093
+ return Storage.get(equip.photo, { level: "public" })
1094
+ .then((result) => {
1095
+ return result;
1096
+ })
1097
+ .catch((error) => {
1098
+ console.log("error fetching file from s3 =>", error);
1099
+ return null;
1100
+ });
1101
+ };
1102
+ const getSignedImageUrlForProfile = async (profile) => {
1103
+ return Storage.get(profile.avatar, { level: "public" })
1104
+ .then((result) => {
1105
+ return result;
1106
+ })
1107
+ .catch((error) => {
1108
+ console.log("error fetching file from s3 =>", error);
1109
+ });
1110
+ };
1111
+ const getSignedImageUrlForSpace = async (space) => {
1112
+ return Storage.get(space.annexes, { level: "public" })
1113
+ .then((result) => {
1114
+ return result;
1115
+ })
1116
+ .catch((error) => {
1117
+ console.log("error fetching file from s3 =>", error);
1118
+ });
1119
+ };
1120
+ const uploadBase64Image = async (uri, objectId, path, friendlyFileName) => {
1121
+ return fetch(uri)
1122
+ .then((res) => res.blob())
1123
+ .then((blob) => {
1124
+ const file = new File([blob], `${friendlyFileName}.jpeg`, {
1125
+ type: "image/jpeg",
1126
+ });
1127
+ return uploadFileToS3(path, file, objectId);
1128
+ });
1129
+ };
1130
+ const uploadBase64ImageWithRetry = async (uri, objectId, path, friendlyFileName, retry) => {
1131
+ return new Promise(async (resolve, reject) => {
1132
+ while (retry > 0) {
1133
+ try {
1134
+ const r = await uploadBase64Image(uri, objectId, path, friendlyFileName);
1135
+ resolve(r);
1136
+ return;
1137
+ }
1138
+ catch (e) {
1139
+ retry--;
1140
+ console.error(`Upload error, retry n° ${retry} : `, e);
1141
+ }
1142
+ }
1143
+ ;
1144
+ reject("Max retry exceeded");
1145
+ });
1146
+ };
1147
+ const removeAllFilesFromFolderS3 = async (folderPath) => {
1148
+ const arrayOfPromises = [];
1149
+ Storage.list(folderPath, { level: "public" })
1150
+ .then((res) => {
1151
+ Object.keys(res).map(async (key) => {
1152
+ const file = res[key];
1153
+ arrayOfPromises.push(deleteFromS3(file.key));
1154
+ });
1155
+ })
1156
+ .catch((error) => console.error("List failure", error));
1157
+ return Promise.all(arrayOfPromises);
1158
+ };
1159
+ const listFilesInFolder = async (folderPath) => {
1160
+ return Storage.list(folderPath, { level: "public", pageSize: 'ALL' })
1161
+ .then((res) => {
1162
+ return res.results;
1163
+ })
1164
+ .catch((error) => console.error("List failure", error));
1165
+ };
1166
+ const getSignedFile = async (path) => {
1167
+ return Storage.get(path, { level: "public" })
1168
+ .then((result) => {
1169
+ return result;
1170
+ })
1171
+ .catch((error) => {
1172
+ console.log("error fetching file from s3 =>", error);
1173
+ return null;
1174
+ });
1175
+ };
1176
+ const downloadFileAsObject = async (path) => {
1177
+ return Storage.get(path, { download: true, level: "public" })
1178
+ .then((result) => {
1179
+ return result;
1180
+ })
1181
+ .catch((error) => {
1182
+ console.log("error fetching file from s3 =>", error);
1183
+ });
1184
+ };
1185
+ const getBufferForFileFromS3 = async (path) => {
1186
+ const object = await downloadFileAsObject(path);
1187
+ if (object) {
1188
+ // @ts-ignore
1189
+ return new ArrayBuffer(object.Body, "binary");
1190
+ }
1191
+ return null;
1192
+ };
1193
+ const uploadJsonToS3 = async (path, json, filename) => {
1194
+ return Storage.put(path + filename, json, {
1195
+ contentType: "application/json",
1196
+ })
1197
+ .then((result) => {
1198
+ console.log("Success =>", result);
1199
+ return result.key;
1200
+ })
1201
+ .catch((error) => {
1202
+ console.log("error =>", error);
1203
+ return null;
1204
+ });
1205
+ };
1206
+ /**
1207
+ * Physically duplicates an S3 object to a new key. Used when copying attachments
1208
+ * between features/tickets/etc. so the copy is fully independent: deleting the
1209
+ * original file later must not affect the copy.
1210
+ * IMPORTANT: does NOT swallow errors — callers need the real error (e.g. NoSuchKey)
1211
+ * to distinguish an orphaned DB reference from a genuine copy failure.
1212
+ */
1213
+ const copyFileInS3 = async (sourceKey, destKey) => {
1214
+ const result = await Storage.copy({ key: sourceKey, level: "public" }, { key: destKey, level: "public" });
1215
+ return result.key;
1216
+ };
1217
+ /**
1218
+ * Fallback duplication: downloads the object and re-uploads it under the new key.
1219
+ *
1220
+ * Storage.copy() builds the S3 `x-amz-copy-source` header by plain string concatenation
1221
+ * (see @aws-amplify/storage AWSS3Provider.copy: `${bucket}/${srcPrefix}${srcKey}`), with no
1222
+ * URL-encoding of the key. A key holding a space, an accent or a parenthesis therefore
1223
+ * yields a header that does not match the signed canonical request, and S3 answers
1224
+ * "The request signature we calculated does not match the signature you provided".
1225
+ *
1226
+ * Round-tripping the bytes avoids that header entirely and works for any key, at the cost
1227
+ * of transferring the file through the browser — hence its use as a fallback only.
1228
+ * IMPORTANT: like copyFileInS3, this must NOT swallow errors.
1229
+ */
1230
+ const copyFileInS3ViaDownload = async (sourceKey, destKey) => {
1231
+ const downloaded = await Storage.get(sourceKey, {
1232
+ download: true,
1233
+ level: "public",
1234
+ });
1235
+ const body = downloaded?.Body;
1236
+ if (!body) {
1237
+ throw new Error(`Empty download for "${sourceKey}", cannot duplicate it`);
1238
+ }
1239
+ const result = await Storage.put(destKey, body, {
1240
+ level: "public",
1241
+ contentType: downloaded.ContentType || undefined,
1242
+ });
1243
+ return result.key;
1244
+ };
1245
+
1045
1246
  class ProfileService {
1046
1247
  API;
1047
1248
  constructor(apiInjected) {
@@ -1987,32 +2188,32 @@ class PoiService {
1987
2188
  if (!sweeps || sweeps.length === 0) {
1988
2189
  return [];
1989
2190
  }
1990
- const queryStatement = `
1991
- query ListPOISLight($filter: ModelPOIFilterInput, $limit: Int, $nextToken: String) {
1992
- listPOIS(filter: $filter, limit: $limit, nextToken: $nextToken) {
1993
- items {
1994
- id
1995
- coordinate
1996
- specific
1997
- private
1998
- global
1999
- type
2000
- elementID
2001
- level
2002
- role
2003
- category
2004
- domainID
2005
- metadata
2006
- tagIcon
2007
- tagDescription
2008
- tagImageID
2009
- matterportSweepID
2010
- createdAt
2011
- updatedAt
2012
- }
2013
- nextToken
2014
- }
2015
- }
2191
+ const queryStatement = `
2192
+ query ListPOISLight($filter: ModelPOIFilterInput, $limit: Int, $nextToken: String) {
2193
+ listPOIS(filter: $filter, limit: $limit, nextToken: $nextToken) {
2194
+ items {
2195
+ id
2196
+ coordinate
2197
+ specific
2198
+ private
2199
+ global
2200
+ type
2201
+ elementID
2202
+ level
2203
+ role
2204
+ category
2205
+ domainID
2206
+ metadata
2207
+ tagIcon
2208
+ tagDescription
2209
+ tagImageID
2210
+ matterportSweepID
2211
+ createdAt
2212
+ updatedAt
2213
+ }
2214
+ nextToken
2215
+ }
2216
+ }
2016
2217
  `;
2017
2218
  const CHUNK_SIZE = 50; // safe margin for an "or" filter expression's practical size
2018
2219
  const chunks = [];
@@ -2779,11 +2980,11 @@ class MatterportPointerService {
2779
2980
  }
2780
2981
  updateCursorDisplay(poseMatterport) {
2781
2982
  if (this.textDisplayCursorPositionPanel && poseMatterport) {
2782
- this.textDisplayCursorPositionPanel.innerHTML = `position:
2783
- ${pointToString(poseMatterport.position)}\n
2784
- normal:
2785
- ${pointToString(poseMatterport.normal)}\n
2786
- floorId:
2983
+ this.textDisplayCursorPositionPanel.innerHTML = `position:
2984
+ ${pointToString(poseMatterport.position)}\n
2985
+ normal:
2986
+ ${pointToString(poseMatterport.normal)}\n
2987
+ floorId:
2787
2988
  ${poseMatterport.floorId}`;
2788
2989
  if (this.getCursorPositionButton) {
2789
2990
  this.getCursorPositionButton.style.display = 'none';
@@ -4794,22 +4995,22 @@ class MatterportService {
4794
4995
  );
4795
4996
  }*/
4796
4997
  addCustomCss() {
4797
- const css = `
4798
- .footer-ui,
4799
- .showcase-footer,
4800
- .mp-nova-btn-group,
4801
- .footer-divider,
4802
- .logo-link,
4803
- .footer-logo,
4804
- .bottom-ui,
4805
- img.logo-image,
4806
- img[class^="logo-image"],
4807
- img[class*=" logo-image"],
4808
- [data-testid="showcase-footer"] {
4809
- opacity: 0 !important;
4810
- visibility: hidden !important;
4811
- pointer-events: none !important;
4812
- }
4998
+ const css = `
4999
+ .footer-ui,
5000
+ .showcase-footer,
5001
+ .mp-nova-btn-group,
5002
+ .footer-divider,
5003
+ .logo-link,
5004
+ .footer-logo,
5005
+ .bottom-ui,
5006
+ img.logo-image,
5007
+ img[class^="logo-image"],
5008
+ img[class*=" logo-image"],
5009
+ [data-testid="showcase-footer"] {
5010
+ opacity: 0 !important;
5011
+ visibility: hidden !important;
5012
+ pointer-events: none !important;
5013
+ }
4813
5014
  `;
4814
5015
  // 1st method: Via Scene SDK
4815
5016
  if (this.sdk?.Scene?.addStyle) {
@@ -5385,65 +5586,1370 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
5385
5586
  args: [MATTERPORT_SDK_KEY]
5386
5587
  }] }, { type: i1$1.Router }, { type: i1$1.ActivatedRoute }, { type: BaseVisibilityService }, { type: i0.NgZone }, { type: MatterportMeasurementService }, { type: MatterportNavigationService }, { type: MatterportPointerService }, { type: MatterportObject3DService }, { type: MatterportTagService }] });
5387
5588
 
5388
- /* eslint-disable no-await-in-loop */
5389
- /* eslint-disable camelcase */
5390
- /* eslint-disable no-underscore-dangle */
5391
- /* eslint-disable class-methods-use-this */
5392
- class ViewerService {
5393
- sdkKey;
5394
- poiService;
5395
- matterportService;
5396
- viewerMode = 'Matterport';
5397
- currentModel3D;
5398
- tourUrl;
5399
- viewerIsOn = new Subject();
5400
- dataIsLoaded = new Subject();
5401
- isLoaded = false;
5402
- showingViewer = false;
5403
- sweepToMove;
5404
- rotationToMove;
5405
- goToTriggered = new Subject();
5406
- toolboxStates = {
5407
- dollhouse: {
5408
- stat: false,
5409
- iconId: 'ui-dollhouse',
5410
- show() {
5411
- },
5412
- hide() {
5413
- },
5414
- },
5415
- floorplan: {
5416
- stat: false,
5417
- },
5418
- mesure: {
5419
- stat: false,
5420
- iconId: 'ui-mesure',
5421
- show() {
5422
- },
5423
- hide() {
5424
- },
5425
- },
5426
- filter: {
5427
- stat: false,
5428
- },
5429
- };
5430
- isObjectLoaded = false;
5431
- constructor(sdkKey, poiService, matterportService) {
5432
- this.sdkKey = sdkKey;
5433
- this.poiService = poiService;
5434
- this.matterportService = matterportService;
5435
- this.setMode('Matterport');
5589
+ /**
5590
+ * Builds an orthonormal frame from three points, or null when they are (nearly) collinear.
5591
+ *
5592
+ * Three points define a plane, and a plane plus an origin defines an orientation. Collinear points
5593
+ * do not: they leave the rotation about their common line undetermined, which is why the caller has
5594
+ * to reject them rather than produce a confident wrong answer.
5595
+ */
5596
+ function frameFromPoints(a, b, c) {
5597
+ const e1 = b.clone().sub(a);
5598
+ if (e1.lengthSq() < 1e-12) {
5599
+ return null;
5436
5600
  }
5437
- /*-----------------------------------------------------------------------------------------------------------------
5438
- Utils
5439
- -----------------------------------------------------------------------------------------------------------------*/
5440
- checkMode(indexMode) {
5441
- return this.viewerMode === indexMode;
5601
+ e1.normalize();
5602
+ const toC = c.clone().sub(a);
5603
+ const e2 = toC.clone().sub(e1.clone().multiplyScalar(toC.dot(e1)));
5604
+ if (e2.lengthSq() < 1e-12) {
5605
+ return null;
5606
+ }
5607
+ e2.normalize();
5608
+ return new THREE.Matrix4().makeBasis(e1, e2, e1.clone().cross(e2));
5609
+ }
5610
+ /**
5611
+ * Solves the transform that maps stored tag coordinates onto the loaded model.
5612
+ *
5613
+ * Takes exactly the correspondences a person can produce by hand: three tags, each pointed at twice
5614
+ * - once where it currently sits, once where it belongs. That is the minimum that pins down all
5615
+ * three parts of the answer. One pair fixes only translation; two leave the rotation about the line
5616
+ * between them free; three resolve it.
5617
+ *
5618
+ * Returns null when the points are collinear or degenerate, because a wrong alignment applied
5619
+ * silently is worse than none.
5620
+ */
5621
+ function solveThreeJsAlignment(pairs) {
5622
+ if (!pairs || pairs.length < 3) {
5623
+ return null;
5624
+ }
5625
+ const sources = pairs.slice(0, 3).map((pair) => new THREE.Vector3(pair.source.x, pair.source.y, pair.source.z));
5626
+ const targets = pairs.slice(0, 3).map((pair) => new THREE.Vector3(pair.target.x, pair.target.y, pair.target.z));
5627
+ const sourceFrame = frameFromPoints(sources[0], sources[1], sources[2]);
5628
+ const targetFrame = frameFromPoints(targets[0], targets[1], targets[2]);
5629
+ if (!sourceFrame || !targetFrame) {
5630
+ return null;
5631
+ }
5632
+ // A rotation's inverse is its transpose, so this is the rotation carrying one frame onto the other.
5633
+ const rotation = targetFrame.multiply(sourceFrame.clone().transpose());
5634
+ // Scale from how far apart the points are in each space, averaged over the three edges so a single
5635
+ // sloppy click does not set it on its own.
5636
+ const edges = [[0, 1], [1, 2], [0, 2]];
5637
+ let ratioSum = 0;
5638
+ let ratioCount = 0;
5639
+ for (const [i, j] of edges) {
5640
+ const sourceLength = sources[i].distanceTo(sources[j]);
5641
+ const targetLength = targets[i].distanceTo(targets[j]);
5642
+ if (sourceLength > 1e-9) {
5643
+ ratioSum += targetLength / sourceLength;
5644
+ ratioCount += 1;
5645
+ }
5646
+ }
5647
+ if (ratioCount === 0) {
5648
+ return null;
5649
+ }
5650
+ const scale = ratioSum / ratioCount;
5651
+ // The group applies position, then rotation, then scale, so a point lands at t + s*R*p. Solving
5652
+ // that for t at the centroids gives the translation.
5653
+ const sourceCentroid = sources.reduce((sum, v) => sum.add(v), new THREE.Vector3()).multiplyScalar(1 / 3);
5654
+ const targetCentroid = targets.reduce((sum, v) => sum.add(v), new THREE.Vector3()).multiplyScalar(1 / 3);
5655
+ const translation = targetCentroid
5656
+ .clone()
5657
+ .sub(sourceCentroid.clone().applyMatrix4(rotation).multiplyScalar(scale));
5658
+ const euler = new THREE.Euler().setFromRotationMatrix(rotation);
5659
+ return {
5660
+ translation: { x: translation.x, y: translation.y, z: translation.z },
5661
+ rotation: { x: euler.x, y: euler.y, z: euler.z },
5662
+ scale,
5663
+ };
5664
+ }
5665
+ class ThreeJsService {
5666
+ ngZone;
5667
+ scene;
5668
+ camera;
5669
+ renderer;
5670
+ controls;
5671
+ animationFrameId;
5672
+ // Raycasting and interaction
5673
+ raycaster = new THREE.Raycaster();
5674
+ mouse = new THREE.Vector2();
5675
+ isDraggingCamera = false;
5676
+ isDraggingTimer;
5677
+ clickableObjects = [];
5678
+ // Observables for the viewer component
5679
+ objectHovered$ = new Subject();
5680
+ objectClicked$ = new Subject();
5681
+ /** Emits when the user clicks a tag, mirroring Matterport's tag click. */
5682
+ tagClicked$ = new Subject();
5683
+ // Tags live under the model rather than the scene, so the centering and scaling applied when the
5684
+ // model is placed carries over and POI coordinates can stay in the model's own space.
5685
+ tagGroup = new THREE.Group();
5686
+ tagSprites = new Map();
5687
+ canvasResizeObserver;
5688
+ /** Aborts the in-flight model download. Parsing cannot be interrupted, only not started. */
5689
+ loadAbortController;
5690
+ // The loaded base model, kept so tag placement can raycast against its geometry.
5691
+ modelRoot;
5692
+ /** True while the next click on the model places a tag instead of orbiting. */
5693
+ placingTag = false;
5694
+ /** Position and normal of the last placed tag, in model space, awaiting commit to a POI. */
5695
+ pendingTagPlacement = null;
5696
+ /** Emits once a tag is dropped on the model, so the UI can confirm and persist it. */
5697
+ tagPlaced$ = new Subject();
5698
+ /** Translucent marker shown under the cursor while placing. */
5699
+ placementPreview;
5700
+ aligning = false;
5701
+ alignmentPairs = [];
5702
+ alignmentSource = null;
5703
+ /** Drives the alignment walkthrough's UI: how many pairs are done and what it wants next. */
5704
+ alignmentProgress$ = new Subject();
5705
+ /** In-flight camera move started by goToTag, advanced by the render loop. */
5706
+ cameraTween = null;
5707
+ /** Emits true while a heavy OBJ file is being parsed (in a worker, when available), false once done. */
5708
+ modelLoading$ = new Subject();
5709
+ /**
5710
+ * Download progress for GLB models, as a 0-1 fraction, or null when the total size is unknown.
5711
+ *
5712
+ * A null means the server did not send Content-Length (chunked responses, some proxies) and no
5713
+ * percentage can be derived; show an indeterminate indicator rather than guessing at one.
5714
+ */
5715
+ modelLoadProgress$ = new Subject();
5716
+ /**
5717
+ * Emits when a load actually stopped short, which is not the same as asking it to.
5718
+ *
5719
+ * cancelModelLoad() can arrive after parse() has taken the main thread, and parse cannot be
5720
+ * interrupted - the model finishes loading regardless. Only this tells the UI which of the two
5721
+ * happened, so it does not offer to reload something that is already on screen.
5722
+ */
5723
+ modelLoadCancelled$ = new Subject();
5724
+ // Background parsing of heavy .obj files, provided by the hosting app (see setObjParserWorker).
5725
+ // Angular libraries built with ng-packagr can't bundle `new Worker(new URL(...))` themselves,
5726
+ // so the app instantiates the worker (its build pipeline supports it) and hands it over here.
5727
+ objParserWorker;
5728
+ objParserPendingRequests = new Map();
5729
+ constructor(ngZone) {
5730
+ this.ngZone = ngZone;
5731
+ }
5732
+ /**
5733
+ * Registers a Web Worker used to parse heavy .obj files off the main thread.
5734
+ * Must be instantiated by the consuming app (e.g. `new Worker(new URL('./obj-parser.worker', import.meta.url))`),
5735
+ * since a library packaged with ng-packagr cannot bundle worker files itself.
5736
+ * When no worker is provided, loadObjModel falls back to parsing on the main thread.
5737
+ */
5738
+ setObjParserWorker(worker) {
5739
+ this.objParserWorker = worker;
5740
+ this.objParserWorker.onmessage = ({ data }) => {
5741
+ const pending = this.objParserPendingRequests.get(data.requestId);
5742
+ if (!pending) {
5743
+ return;
5744
+ }
5745
+ this.objParserPendingRequests.delete(data.requestId);
5746
+ if (data.success && data.meshes) {
5747
+ pending.resolve(data.meshes);
5748
+ }
5749
+ else {
5750
+ pending.reject(new Error(data.error || 'Unknown error while parsing OBJ file in worker'));
5751
+ }
5752
+ };
5753
+ }
5754
+ requestObjParseInWorker(objUrl) {
5755
+ return new Promise((resolve, reject) => {
5756
+ const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
5757
+ this.objParserPendingRequests.set(requestId, { resolve, reject });
5758
+ this.objParserWorker.postMessage({ requestId, objUrl });
5759
+ // Diagnostic only: large models legitimately take a while, this isn't a timeout/abort,
5760
+ // just a signal in case the worker went silent (e.g. it ran out of memory mid-parse).
5761
+ setTimeout(() => {
5762
+ if (this.objParserPendingRequests.has(requestId)) {
5763
+ console.warn(`ThreeJsService: obj-parser worker has not replied after 60s for request ${requestId} (objUrl=${objUrl}). Still waiting - large files can take a while, but this may also mean the worker hung or ran out of memory.`);
5764
+ }
5765
+ }, 60000);
5766
+ });
5767
+ }
5768
+ /** Rebuilds a THREE.Mesh from the raw typed arrays sent back by the parser worker. */
5769
+ meshFromParsedData(data, materials) {
5770
+ const geometry = new THREE.BufferGeometry();
5771
+ geometry.setAttribute('position', new THREE.BufferAttribute(data.position, 3));
5772
+ if (data.uv) {
5773
+ geometry.setAttribute('uv', new THREE.BufferAttribute(data.uv, 2));
5774
+ }
5775
+ // Index has to be set before any normal computation, which reads it to average shared faces.
5776
+ if (data.index) {
5777
+ geometry.setIndex(new THREE.BufferAttribute(data.index, 1));
5778
+ }
5779
+ if (data.normal) {
5780
+ geometry.setAttribute('normal', new THREE.BufferAttribute(data.normal, 3));
5781
+ }
5782
+ else {
5783
+ geometry.computeVertexNormals();
5784
+ }
5785
+ // Box3.setFromObject() reuses an already-set boundingBox instead of walking every vertex,
5786
+ // so adopting the worker's makes framing the camera cheap no matter how dense the mesh is.
5787
+ if (data.boundingBox) {
5788
+ geometry.boundingBox = new THREE.Box3(new THREE.Vector3(...data.boundingBox.min), new THREE.Vector3(...data.boundingBox.max));
5789
+ }
5790
+ const resolvedMaterials = data.materialNames.map((name) => {
5791
+ const material = materials && name ? materials.create(name) : undefined;
5792
+ if (!material) {
5793
+ console.warn(`ThreeJsService: no MTL material named "${name}" for mesh "${data.name}", using a plain fallback (no texture).`);
5794
+ }
5795
+ return material || new THREE.MeshPhongMaterial();
5796
+ });
5797
+ let mesh;
5798
+ if (data.groups && data.groups.length > 0) {
5799
+ data.groups.forEach((group) => geometry.addGroup(group.start, group.count, group.materialIndex));
5800
+ mesh = new THREE.Mesh(geometry, resolvedMaterials);
5801
+ }
5802
+ else {
5803
+ mesh = new THREE.Mesh(geometry, resolvedMaterials[0]);
5804
+ }
5805
+ mesh.name = data.name;
5806
+ return mesh;
5807
+ }
5808
+ init(canvas) {
5809
+ // This service is a singleton but the viewer component is rebuilt on every navigation, so state
5810
+ // from the previous scene is still here. Two things outlive it and have to go first:
5811
+ //
5812
+ // - A download still in flight. Its onLoaded resolves against whatever `this.scene` points at
5813
+ // by then, so the model being abandoned would be handed to the scene built below.
5814
+ // - The tag group, which is a field and keeps its sprites. They would be re-parented under the
5815
+ // new model and show the previous visit's tags on it.
5816
+ this.discardCurrentLoad();
5817
+ this.clearTags();
5818
+ this.cameraTween = null;
5819
+ this.modelRoot = undefined;
5820
+ // 1. Setup Scene
5821
+ this.scene = new THREE.Scene();
5822
+ this.scene.background = new THREE.Color(0xe0f7ff); // from POC
5823
+ // 2. Setup Camera
5824
+ this.camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 10000);
5825
+ this.camera.position.set(0, 5, 10); // will be overridden later if passed
5826
+ // 3. Setup WebGL Renderer
5827
+ this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
5828
+ this.renderer.setSize(canvas.clientWidth, canvas.clientHeight);
5829
+ this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
5830
+ this.observeCanvasResize(canvas);
5831
+ // Fix for Three.js r147 vs r152+ color management
5832
+ if ('outputColorSpace' in this.renderer) {
5833
+ this.renderer.outputColorSpace = 'srgb'; // THREE.SRGBColorSpace
5834
+ }
5835
+ else {
5836
+ this.renderer.outputEncoding = 3001; // THREE.sRGBEncoding
5837
+ }
5838
+ // Without tone mapping anything brighter than 1.0 is simply cut off to pure white. Captured
5839
+ // textures carry a small fraction of very bright pixels - sky reflected in windows, sunlit
5840
+ // render - and once lighting pushes those past the limit they flatten into white specks with
5841
+ // no detail left. Rolling the highlights off instead keeps that detail and keeps bright areas
5842
+ // reading as bright rather than blown.
5843
+ this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
5844
+ this.renderer.toneMappingExposure = 1.0;
5845
+ // 4. Lighting, kept deliberately restrained and neutral.
5846
+ //
5847
+ // Captured/photogrammetry models already have lighting baked into their textures (the sun and
5848
+ // shadows came with the photos), so lighting them again distorts what they actually look like.
5849
+ // The intensities below add up to roughly 1.0 on a surface facing the viewer, which renders the
5850
+ // texture at its true value. Anything above 1.0 is clipped to pure white by the renderer, which
5851
+ // is what turns bright areas into flat white patches with no detail left in them.
5852
+ //
5853
+ // Neutral white is used rather than a warm tint for the same reason: a tinted light shifts every
5854
+ // colour in the model away from what was captured.
5855
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
5856
+ this.scene.add(ambientLight);
5857
+ // Headlight rather than a fixed sun: parented to the camera so it always points at whatever is
5858
+ // being looked at, giving shape to surfaces without ever leaving a face unlit. No shadow casting
5859
+ // - shadows would double up with the ones already baked into the textures, and the shadow map
5860
+ // was a real cost per frame.
5861
+ const headLight = new THREE.DirectionalLight(0xffffff, 0.35);
5862
+ headLight.position.set(0, 0, 1);
5863
+ this.camera.add(headLight);
5864
+ // A light only contributes while it is part of the scene graph, and this one hangs off the camera.
5865
+ this.scene.add(this.camera);
5866
+ // 5. Setup Controls (from POC)
5867
+ this.controls = new OrbitControls(this.camera, this.renderer.domElement);
5868
+ this.controls.rotateSpeed = 1.0;
5869
+ this.controls.dampingFactor = 0.1;
5870
+ this.controls.enableDamping = true;
5871
+ this.controls.mouseButtons = {
5872
+ LEFT: THREE.MOUSE.ROTATE,
5873
+ MIDDLE: THREE.MOUSE.DOLLY,
5874
+ RIGHT: THREE.MOUSE.PAN,
5875
+ };
5876
+ this.controls.target.set(0, 0, 0);
5877
+ // Event listeners on controls to differentiate dragging vs clicking
5878
+ this.controls.addEventListener('start', () => {
5879
+ this.isDraggingTimer = setTimeout(() => {
5880
+ this.isDraggingCamera = true;
5881
+ }, 150);
5882
+ });
5883
+ this.controls.addEventListener('end', () => {
5884
+ clearTimeout(this.isDraggingTimer);
5885
+ if (this.isDraggingCamera) {
5886
+ setTimeout(() => {
5887
+ this.isDraggingCamera = false;
5888
+ }, 250);
5889
+ }
5890
+ });
5891
+ // Interaction Event Listeners on canvas
5892
+ canvas.addEventListener('click', this.onMouseClick.bind(this), false);
5893
+ canvas.addEventListener('mousemove', this.onMouseMove.bind(this), false);
5894
+ // 6. Start the render loop
5895
+ this.ngZone.runOutsideAngular(() => {
5896
+ this.animate();
5897
+ });
5898
+ }
5899
+ /**
5900
+ * Path (served by the app) holding basis_transcoder.js/.wasm, which KTX2Loader needs to turn
5901
+ * Basis Universal textures into whatever compressed format the local GPU actually supports.
5902
+ */
5903
+ ktx2TranscoderPath = 'assets/basis/';
5904
+ /** Overrides where the Basis transcoder files are served from. */
5905
+ setKtx2TranscoderPath(path) {
5906
+ this.ktx2TranscoderPath = path;
5907
+ }
5908
+ /**
5909
+ * Loads a base 3D model in GLTF/GLB format - the preferred format for large models.
5910
+ * Unlike .obj, geometry arrives as binary typed arrays (no text parsing to offload to a worker)
5911
+ * and stays indexed, so far less data reaches the GPU. KTX2 textures are supported too: they
5912
+ * stay compressed in VRAM instead of being decoded to raw pixels like JPEG/PNG.
5913
+ */
5914
+ loadGLTFModel(url, cameraPosition, rotation) {
5915
+ const loader = new GLTFLoader();
5916
+ // KTX2 needs the renderer to report which compressed formats this GPU accepts, so this has to
5917
+ // happen after init(). Guarded so a model without KTX2 textures still loads if it's missing.
5918
+ if (this.renderer) {
5919
+ try {
5920
+ const ktx2Loader = new KTX2Loader()
5921
+ .setTranscoderPath(this.ktx2TranscoderPath)
5922
+ .detectSupport(this.renderer);
5923
+ loader.setKTX2Loader(ktx2Loader);
5924
+ }
5925
+ catch (error) {
5926
+ console.warn('ThreeJsService: could not set up KTX2 texture support, falling back to uncompressed textures.', error);
5927
+ }
5928
+ }
5929
+ // Lets gltfpack/gltf-transform output using EXT_meshopt_compression load without extra setup.
5930
+ loader.setMeshoptDecoder(MeshoptDecoder);
5931
+ this.modelLoading$.next(true);
5932
+ this.modelLoadProgress$.next(0);
5933
+ // Order matters: normals must exist before anisotropy, and the Phong conversion has to run
5934
+ // before side/flatShading are touched so those settings land on the materials we keep.
5935
+ const onLoaded = (gltf) => {
5936
+ this.convertToPhongMaterials(gltf.scene);
5937
+ this.forceDoubleSidedMaterials(gltf.scene);
5938
+ this.ensureVertexNormals(gltf.scene);
5939
+ this.tuneTextureSampling(gltf.scene);
5940
+ this.placeLoadedObjectInScene(gltf.scene, cameraPosition, rotation);
5941
+ this.modelLoading$.next(false);
5942
+ };
5943
+ const onError = (error) => {
5944
+ // Cancelling is a decision, not a failure: clear the overlay without an error in the console.
5945
+ if (error?.name === 'AbortError') {
5946
+ // Only a cancel of the load that is still current came from the user; anything else is this
5947
+ // load having been superseded or discarded.
5948
+ if (this.loadAbortController?.signal.aborted) {
5949
+ this.modelLoadCancelled$.next();
5950
+ }
5951
+ }
5952
+ else {
5953
+ console.error('Error loading GLTF model:', error);
5954
+ }
5955
+ this.modelLoading$.next(false);
5956
+ this.modelLoadProgress$.next(null);
5957
+ };
5958
+ // GLTFLoader decodes embedded images with createImageBitmap when the browser offers it, and on
5959
+ // large texture atlases that path returns visibly corrupt data - scattered white blocks across
5960
+ // the model. The exact same bytes decoded through an HTMLImageElement, which is what the .obj
5961
+ // path uses via MTLLoader, come out clean. GLTFLoader already falls back to that decoder for
5962
+ // Safari and older Firefox; this simply extends the fallback to cover this case too.
5963
+ //
5964
+ // The decoder is chosen while GLTFParser is being constructed, which happens synchronously
5965
+ // inside parse(), so hiding createImageBitmap only has to hold across that call rather than
5966
+ // for the whole download.
5967
+ this.fetchWithProgress(url)
5968
+ // parse() holds the main thread for seconds on a large model. Yield until the browser has
5969
+ // actually painted first, otherwise the final progress update never reaches the screen and
5970
+ // the bar appears to stall short of the end.
5971
+ .then((buffer) => this.afterNextPaint().then(() => buffer))
5972
+ .then((buffer) => {
5973
+ // parse() cannot be interrupted, so a cancel that lands during the paint yield has to be
5974
+ // caught here - the last moment before the main thread is committed for several seconds.
5975
+ if (this.loadAbortController?.signal.aborted) {
5976
+ throw new DOMException('Model load cancelled', 'AbortError');
5977
+ }
5978
+ const globalScope = globalThis;
5979
+ const originalCreateImageBitmap = globalScope.createImageBitmap;
5980
+ globalScope.createImageBitmap = undefined;
5981
+ try {
5982
+ loader.parse(buffer, url.substring(0, url.lastIndexOf('/') + 1), onLoaded, onError);
5983
+ }
5984
+ finally {
5985
+ globalScope.createImageBitmap = originalCreateImageBitmap;
5986
+ }
5987
+ })
5988
+ .catch(onError);
5989
+ }
5990
+ /**
5991
+ * Stops the model download in progress, if any.
5992
+ *
5993
+ * Only the download can be interrupted: parse() runs synchronously and holds the main thread until
5994
+ * it finishes, so once it has begun there is nothing to cancel. In practice the download is the
5995
+ * long part, which is where someone reaches for this.
5996
+ */
5997
+ cancelModelLoad() {
5998
+ this.loadAbortController?.abort();
5999
+ }
6000
+ /**
6001
+ * Abandons the load in progress without reporting it as cancelled.
6002
+ *
6003
+ * Dropping the controller is what makes the difference: onError treats an abort as user-initiated
6004
+ * only while the controller that aborted is still the current one. Tearing down or rebuilding the
6005
+ * scene is not something the person asked for, so it must not surface as "loading cancelled".
6006
+ */
6007
+ discardCurrentLoad() {
6008
+ this.loadAbortController?.abort();
6009
+ this.loadAbortController = undefined;
6010
+ }
6011
+ /**
6012
+ * Resolves once the browser has painted at least one frame.
6013
+ *
6014
+ * A requestAnimationFrame callback runs *before* that frame is painted, so a single one is not
6015
+ * enough to guarantee pending DOM changes are on screen; the second fires after the first frame
6016
+ * has gone out. Falls back to a timeout where rAF isn't available (SSR, tests).
6017
+ */
6018
+ afterNextPaint() {
6019
+ if (typeof requestAnimationFrame !== 'function') {
6020
+ return new Promise((resolve) => setTimeout(resolve, 0));
6021
+ }
6022
+ return new Promise((resolve) => {
6023
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
6024
+ });
6025
+ }
6026
+ /**
6027
+ * Downloads a file, reporting progress on modelLoadProgress$ as it arrives.
6028
+ *
6029
+ * GLTFLoader.load() would report progress through XHR, but this path deliberately uses fetch() +
6030
+ * parse() instead (see loadGLTFModel for why), so progress is read off the response stream here.
6031
+ * That is also the more useful number: on a model of this size the download dominates, and the
6032
+ * stream reports it as it happens rather than in one jump at the end.
6033
+ */
6034
+ async fetchWithProgress(url) {
6035
+ // A load already running is replaced, not raced: whichever finished last would otherwise win.
6036
+ this.loadAbortController?.abort();
6037
+ this.loadAbortController = new AbortController();
6038
+ const { signal } = this.loadAbortController;
6039
+ const response = await fetch(url, { signal });
6040
+ if (!response.ok) {
6041
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
6042
+ }
6043
+ const contentLength = Number(response.headers.get('Content-Length'));
6044
+ const total = Number.isFinite(contentLength) && contentLength > 0 ? contentLength : 0;
6045
+ // Without a readable stream (older browsers, some polyfills) there is nothing to measure, so
6046
+ // fall back to a plain buffered read and let the UI show an indeterminate indicator.
6047
+ if (!response.body?.getReader) {
6048
+ this.modelLoadProgress$.next(null);
6049
+ return response.arrayBuffer();
6050
+ }
6051
+ const reader = response.body.getReader();
6052
+ // With a known size, write chunks straight into one pre-allocated buffer. Collecting them in an
6053
+ // array and concatenating afterwards would hold both copies at once - on a 164 MB model that is
6054
+ // an avoidable 300+ MB peak.
6055
+ const buffer = total > 0 ? new Uint8Array(total) : null;
6056
+ const chunks = [];
6057
+ let usingBuffer = buffer !== null;
6058
+ let received = 0;
6059
+ // Chunks arrive in tens of kilobytes, so a 164 MB model produces thousands of them. Emitting on
6060
+ // every one would run change detection just as often to move a bar that is measured in whole
6061
+ // percent, so only report when the displayed value would actually change.
6062
+ let lastReported = -1;
6063
+ if (total === 0) {
6064
+ this.modelLoadProgress$.next(null);
6065
+ }
6066
+ for (;;) {
6067
+ const { done, value } = await reader.read();
6068
+ if (done) {
6069
+ break;
6070
+ }
6071
+ if (signal.aborted) {
6072
+ // Free the connection rather than leaving it draining in the background.
6073
+ await reader.cancel();
6074
+ throw new DOMException('Model load cancelled', 'AbortError');
6075
+ }
6076
+ if (usingBuffer && received + value.length > total) {
6077
+ // Content-Length understated the real size. Move what we have into the chunk list and stay
6078
+ // there for the rest of the response, rather than truncating the tail.
6079
+ chunks.push(buffer.slice(0, received));
6080
+ usingBuffer = false;
6081
+ }
6082
+ if (usingBuffer) {
6083
+ buffer.set(value, received);
6084
+ }
6085
+ else {
6086
+ chunks.push(value);
6087
+ }
6088
+ received += value.length;
6089
+ if (total > 0) {
6090
+ const percent = Math.min(Math.round((received / total) * 100), 100);
6091
+ if (percent !== lastReported) {
6092
+ lastReported = percent;
6093
+ this.modelLoadProgress$.next(percent / 100);
6094
+ }
6095
+ }
6096
+ }
6097
+ // The stream can end below the advertised size (or with no size at all). Land on a full bar
6098
+ // rather than leaving it stuck at whatever the last chunk happened to reach.
6099
+ if (total > 0 && lastReported !== 100) {
6100
+ this.modelLoadProgress$.next(1);
6101
+ }
6102
+ if (usingBuffer && received === total) {
6103
+ return buffer.buffer;
6104
+ }
6105
+ if (usingBuffer) {
6106
+ // Fewer bytes than advertised: hand back exactly what arrived.
6107
+ return buffer.buffer.slice(0, received);
6108
+ }
6109
+ const merged = new Uint8Array(received);
6110
+ let offset = 0;
6111
+ chunks.forEach((chunk) => {
6112
+ merged.set(chunk, offset);
6113
+ offset += chunk.length;
6114
+ });
6115
+ return merged.buffer;
6116
+ }
6117
+ /**
6118
+ * Fits the camera's clipping planes to how big the model actually is.
6119
+ *
6120
+ * Depth precision depends on the far/near ratio, and the fixed 0.1 / 10000 the camera starts with
6121
+ * is a ratio of 100000 for a model only ~10 units across. That wastes most of the depth buffer on
6122
+ * empty space and lets nearly coincident surfaces - which captured meshes have everywhere -
6123
+ * flicker against each other.
6124
+ */
6125
+ adjustClippingPlanes(radius) {
6126
+ if (radius < 10) {
6127
+ this.camera.near = 0.01;
6128
+ this.camera.far = 100;
6129
+ }
6130
+ else if (radius < 100) {
6131
+ this.camera.near = 0.1;
6132
+ this.camera.far = 1000;
6133
+ }
6134
+ else if (radius < 1000) {
6135
+ this.camera.near = 10;
6136
+ this.camera.far = 10000;
6137
+ }
6138
+ else {
6139
+ this.camera.near = 100;
6140
+ this.camera.far = 1000000;
6141
+ }
6142
+ this.camera.updateProjectionMatrix();
6143
+ }
6144
+ /**
6145
+ * Swaps the PBR materials GLTFLoader builds for the plain Phong ones the .obj path produces.
6146
+ *
6147
+ * MeshStandardMaterial keeps a specular reflection even at metalness 0, since every dielectric
6148
+ * reflects some light, and Fresnel drives that reflection towards white as a surface turns away
6149
+ * from the viewer. On an aerial view of a facade most surfaces sit at exactly those grazing
6150
+ * angles, which paints white highlights over window frames and roof edges - the same model
6151
+ * loaded as .obj never shows them, because MTLLoader builds Phong materials and OBJLoader zeroes
6152
+ * their specular when the .mtl declares Ns 0.
6153
+ *
6154
+ * Captured models carry their lighting in the texture and want no specular response at all, so
6155
+ * matching the .obj path here is both the fix and the more faithful choice.
6156
+ */
6157
+ convertToPhongMaterials(root) {
6158
+ const converted = new Map();
6159
+ root.traverse((child) => {
6160
+ const mesh = child;
6161
+ if (!mesh.isMesh) {
6162
+ return;
6163
+ }
6164
+ const convert = (source) => {
6165
+ if (!source || !source.isMeshStandardMaterial) {
6166
+ return source;
6167
+ }
6168
+ if (!converted.has(source)) {
6169
+ const phong = new THREE.MeshPhongMaterial({
6170
+ map: source.map,
6171
+ color: source.color,
6172
+ transparent: source.transparent,
6173
+ opacity: source.opacity,
6174
+ alphaTest: source.alphaTest,
6175
+ side: source.side,
6176
+ specular: new THREE.Color(0x000000),
6177
+ shininess: 0,
6178
+ });
6179
+ phong.name = source.name;
6180
+ converted.set(source, phong);
6181
+ }
6182
+ return converted.get(source);
6183
+ };
6184
+ mesh.material = Array.isArray(mesh.material)
6185
+ ? mesh.material.map(convert)
6186
+ : convert(mesh.material);
6187
+ });
6188
+ }
6189
+ /**
6190
+ * Renders every material double-sided.
6191
+ *
6192
+ * Reality capture meshes routinely contain triangles with reversed winding - an artefact of the
6193
+ * reconstruction, not of the file. With the default FrontSide those triangles are culled, leaving
6194
+ * scattered holes that show the scene background through the model. Against a light background
6195
+ * they read as white specks with no discernible pattern.
6196
+ *
6197
+ * Drawing both sides costs a little fill rate but is what makes captured geometry render as
6198
+ * solid; other viewers of this kind of content do the same unconditionally.
6199
+ */
6200
+ forceDoubleSidedMaterials(root) {
6201
+ const seen = new Set();
6202
+ root.traverse((child) => {
6203
+ const mesh = child;
6204
+ if (!mesh.isMesh) {
6205
+ return;
6206
+ }
6207
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
6208
+ materials.forEach((material) => {
6209
+ if (material && !seen.has(material)) {
6210
+ seen.add(material);
6211
+ material.side = THREE.DoubleSide;
6212
+ material.needsUpdate = true;
6213
+ }
6214
+ });
6215
+ });
6216
+ }
6217
+ /**
6218
+ * Computes real vertex normals for any geometry that arrived without them.
6219
+ *
6220
+ * A glTF without a NORMAL attribute is spec-compliant, and GLTFLoader handles it by switching the
6221
+ * material to flatShading, which makes the shader derive the normal per pixel from screen-space
6222
+ * derivatives. That derivation collapses on the degenerate, needle-thin triangles that reality
6223
+ * capture meshes are full of: the face has almost no area, the derived normal is meaningless, and
6224
+ * lighting blows those fragments out to white specks - densest exactly where fine detail is.
6225
+ *
6226
+ * Computing normals from the geometry instead is robust to those triangles, since a degenerate
6227
+ * face contributes almost nothing to the average and its neighbours dominate.
6228
+ */
6229
+ ensureVertexNormals(root) {
6230
+ root.traverse((child) => {
6231
+ const mesh = child;
6232
+ if (!mesh.isMesh) {
6233
+ return;
6234
+ }
6235
+ const geometry = mesh.geometry;
6236
+ if (geometry.getAttribute('normal')) {
6237
+ return;
6238
+ }
6239
+ geometry.computeVertexNormals();
6240
+ // GLTFLoader already switched these materials to flatShading because normals were missing;
6241
+ // now that real ones exist, let them be used instead of the derivative fallback.
6242
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
6243
+ materials.forEach((material) => {
6244
+ if (material && material.flatShading) {
6245
+ material.flatShading = false;
6246
+ material.needsUpdate = true;
6247
+ }
6248
+ });
6249
+ });
6250
+ }
6251
+ /**
6252
+ * Configures how every texture in a model is sampled: maximum anisotropy, and no mipmapping.
6253
+ *
6254
+ * Photogrammetry atlases pack their UV islands with unused gutter pixels between them. Mipmapping
6255
+ * averages neighbouring pixels at each level, so those gutters bleed further into the islands the
6256
+ * smaller the level gets - and viewing the whole model at once uses the smallest levels, painting
6257
+ * bright gutter pixels over the edges of fine detail. Sampling the full-resolution image avoids
6258
+ * that, at the cost of some shimmer on minified surfaces.
6259
+ *
6260
+ * Note that anisotropy only takes effect while mipmapping is on, so with the settings below it is
6261
+ * inert; it is set anyway so the sampling stays correct for anyone who re-enables mipmaps here.
6262
+ */
6263
+ tuneTextureSampling(root) {
6264
+ if (!this.renderer) {
6265
+ return;
6266
+ }
6267
+ const maxAnisotropy = this.renderer.capabilities.getMaxAnisotropy();
6268
+ if (!maxAnisotropy || maxAnisotropy <= 1) {
6269
+ return;
6270
+ }
6271
+ const seen = new Set();
6272
+ root.traverse((child) => {
6273
+ const mesh = child;
6274
+ if (!mesh.isMesh) {
6275
+ return;
6276
+ }
6277
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
6278
+ materials.forEach((material) => {
6279
+ if (!material) {
6280
+ return;
6281
+ }
6282
+ // Same texture is reused across the model's materials; only touch each one once.
6283
+ ['map', 'normalMap', 'roughnessMap', 'metalnessMap', 'emissiveMap', 'aoMap'].forEach((slot) => {
6284
+ const texture = material[slot];
6285
+ if (texture && !seen.has(texture)) {
6286
+ seen.add(texture);
6287
+ texture.anisotropy = maxAnisotropy;
6288
+ texture.generateMipmaps = false;
6289
+ texture.minFilter = THREE.LinearFilter;
6290
+ texture.needsUpdate = true;
6291
+ }
6292
+ });
6293
+ });
6294
+ });
6295
+ }
6296
+ /**
6297
+ * Loads an OBJ model with MTL.
6298
+ * The heavy text-parsing step runs in a Web Worker when one has been registered via
6299
+ * setObjParserWorker (see there for why the app, not this library, provides it); this keeps
6300
+ * the main thread free while large architecture models (millions of vertices) are parsed.
6301
+ * Falls back to parsing on the main thread otherwise.
6302
+ */
6303
+ loadObjModel(mtlUrl, objUrl, cameraPosition, rotation) {
6304
+ const mtlLoader = new MTLLoader();
6305
+ const basePath = mtlUrl.substring(0, mtlUrl.lastIndexOf('/') + 1);
6306
+ mtlLoader.setPath(basePath);
6307
+ const mtlFileName = mtlUrl.substring(mtlUrl.lastIndexOf('/') + 1);
6308
+ this.modelLoading$.next(true);
6309
+ mtlLoader.load(mtlFileName, (materials) => {
6310
+ materials.preload();
6311
+ if (this.objParserWorker) {
6312
+ this.requestObjParseInWorker(objUrl)
6313
+ .then((meshes) => {
6314
+ const group = new THREE.Group();
6315
+ meshes.forEach((meshData) => {
6316
+ group.add(this.meshFromParsedData(meshData, materials));
6317
+ });
6318
+ this.placeLoadedObjectInScene(group, cameraPosition, rotation);
6319
+ })
6320
+ .catch((error) => {
6321
+ console.error('Error parsing OBJ model in worker:', error);
6322
+ })
6323
+ .finally(() => this.modelLoading$.next(false));
6324
+ return;
6325
+ }
6326
+ const objLoader = new OBJLoader();
6327
+ objLoader.setMaterials(materials);
6328
+ objLoader.load(objUrl, (object) => {
6329
+ this.placeLoadedObjectInScene(object, cameraPosition, rotation);
6330
+ this.modelLoading$.next(false);
6331
+ }, undefined, (error) => {
6332
+ console.error('Error loading OBJ model:', error);
6333
+ this.modelLoading$.next(false);
6334
+ });
6335
+ }, undefined, (error) => {
6336
+ console.error('Error loading MTL file:', error);
6337
+ this.modelLoading$.next(false);
6338
+ });
6339
+ }
6340
+ /** Centers, scales and adds a loaded object to the scene, then frames the camera on it. */
6341
+ placeLoadedObjectInScene(object, cameraPosition, rotation) {
6342
+ const box = new THREE.Box3().setFromObject(object);
6343
+ const center = box.getCenter(new THREE.Vector3());
6344
+ const size = box.getSize(new THREE.Vector3());
6345
+ const maxDim = Math.max(size.x, size.y, size.z);
6346
+ const scale = maxDim > 0 ? 10 / maxDim : 1;
6347
+ // Two nested groups so tags can share the model's frame: `centered` carries the offset that
6348
+ // moves the model's own origin to the scene origin, `group` carries the scale. A tag positioned
6349
+ // with raw POI coordinates then lands in the same place the model's geometry does.
6350
+ const group = new THREE.Group();
6351
+ const centered = new THREE.Group();
6352
+ centered.position.sub(center);
6353
+ if (rotation) {
6354
+ object.rotation.set(rotation.x, rotation.y, rotation.z);
6355
+ }
6356
+ this.modelRoot = object;
6357
+ centered.add(object);
6358
+ centered.add(this.tagGroup);
6359
+ group.add(centered);
6360
+ group.scale.setScalar(scale);
6361
+ this.scene.add(group);
6362
+ // Tags registered before the model finished loading were sized against the old scale.
6363
+ this.tagSprites.forEach((sprite) => this.scaleTagForModel(sprite));
6364
+ this.adjustClippingPlanes((size.length() / 2) * scale);
6365
+ if (cameraPosition) {
6366
+ this.camera.position.set(cameraPosition.x, cameraPosition.y, cameraPosition.z);
6367
+ }
6368
+ else {
6369
+ this.camera.position.set(0, 5, 15);
6370
+ }
6371
+ this.controls.target.set(0, 0, 0);
6372
+ this.controls.update();
6373
+ }
6374
+ /**
6375
+ * Keeps the drawing buffer and camera matched to the canvas as it is resized.
6376
+ *
6377
+ * renderer.setSize() writes explicit pixel width/height into the canvas's style, overriding the
6378
+ * 100%/100% it starts with. Without this, the canvas keeps whatever size it had at startup: hiding
6379
+ * the top menu grows the container, the canvas does not follow, and the strip that opens up shows
6380
+ * the page behind it instead of the scene.
6381
+ *
6382
+ * The parent is observed rather than the canvas, because resizing the canvas is what this reacts
6383
+ * to - watching the element we resize would retrigger the observer.
6384
+ */
6385
+ observeCanvasResize(canvas) {
6386
+ const container = canvas.parentElement;
6387
+ if (!container || typeof ResizeObserver === 'undefined') {
6388
+ return;
6389
+ }
6390
+ this.canvasResizeObserver = new ResizeObserver(() => {
6391
+ const width = container.clientWidth;
6392
+ const height = container.clientHeight;
6393
+ if (width === 0 || height === 0) {
6394
+ return;
6395
+ }
6396
+ this.camera.aspect = width / height;
6397
+ this.camera.updateProjectionMatrix();
6398
+ this.renderer.setSize(width, height);
6399
+ });
6400
+ this.canvasResizeObserver.observe(container);
6401
+ }
6402
+ /**
6403
+ * Starts the alignment walkthrough: pick a tag, then click where it really is, three times.
6404
+ *
6405
+ * Existing tags are the correspondences - they already carry the stored coordinate, so the person
6406
+ * only has to supply the matching spot on the model. No new records are written; nothing is
6407
+ * applied until solveAlignmentFromPairs() succeeds.
6408
+ */
6409
+ startTagAlignment() {
6410
+ this.aligning = true;
6411
+ this.alignmentPairs = [];
6412
+ this.alignmentSource = null;
6413
+ if (this.renderer) {
6414
+ this.renderer.domElement.style.cursor = 'crosshair';
6415
+ }
6416
+ this.alignmentProgress$.next({ pairs: 0, awaiting: 'tag' });
6417
+ }
6418
+ cancelTagAlignment() {
6419
+ this.aligning = false;
6420
+ this.alignmentPairs = [];
6421
+ this.alignmentSource = null;
6422
+ if (this.renderer) {
6423
+ this.renderer.domElement.style.cursor = 'default';
6424
+ }
6425
+ this.alignmentProgress$.next({ pairs: 0, awaiting: null });
6426
+ }
6427
+ isAligningTags() {
6428
+ return this.aligning;
6429
+ }
6430
+ /**
6431
+ * Solves and applies the alignment collected so far, returning it so the caller can persist it.
6432
+ *
6433
+ * Returns null when the three tags turn out to be collinear, in which case nothing is applied and
6434
+ * the walkthrough stays open for the person to pick a better third point.
6435
+ */
6436
+ solveAlignmentFromPairs() {
6437
+ const alignment = solveThreeJsAlignment(this.alignmentPairs);
6438
+ if (!alignment) {
6439
+ return null;
6440
+ }
6441
+ this.setTagAlignment(alignment);
6442
+ this.aligning = false;
6443
+ this.renderer.domElement.style.cursor = 'default';
6444
+ this.alignmentProgress$.next({ pairs: this.alignmentPairs.length, awaiting: null });
6445
+ return alignment;
6446
+ }
6447
+ /** Handles a click while the alignment walkthrough is running. */
6448
+ handleAlignmentClick(event) {
6449
+ if (!this.alignmentSource) {
6450
+ // First half of a pair: which tag are we talking about?
6451
+ const hit = this.raycaster.intersectObjects(this.tagGroup.children, false)[0];
6452
+ if (!hit) {
6453
+ return;
6454
+ }
6455
+ // The sprite's own position is the stored coordinate, untouched by any alignment already
6456
+ // applied - that transform lives on the group, not on the sprite.
6457
+ const stored = hit.object.position;
6458
+ this.alignmentSource = { x: stored.x, y: stored.y, z: stored.z };
6459
+ this.alignmentProgress$.next({
6460
+ pairs: this.alignmentPairs.length,
6461
+ awaiting: 'point',
6462
+ tagId: hit.object.name,
6463
+ });
6464
+ return;
6465
+ }
6466
+ // Second half: where that tag actually belongs on the model.
6467
+ const placement = this.pickPlacementOnModel(event);
6468
+ if (!placement) {
6469
+ return;
6470
+ }
6471
+ this.alignmentPairs.push({ source: this.alignmentSource, target: placement.position });
6472
+ this.alignmentSource = null;
6473
+ this.alignmentProgress$.next({
6474
+ pairs: this.alignmentPairs.length,
6475
+ awaiting: this.alignmentPairs.length >= 3 ? null : 'tag',
6476
+ });
6477
+ }
6478
+ /** Arms tag placement: the next click on the model drops a tag instead of being ignored. */
6479
+ startTagPlacement() {
6480
+ this.placingTag = true;
6481
+ this.pendingTagPlacement = null;
6482
+ if (this.renderer) {
6483
+ this.renderer.domElement.style.cursor = 'crosshair';
6484
+ }
6485
+ }
6486
+ /** Leaves placement mode without placing anything. */
6487
+ cancelTagPlacement() {
6488
+ this.placingTag = false;
6489
+ this.pendingTagPlacement = null;
6490
+ this.removePlacementPreview();
6491
+ if (this.renderer) {
6492
+ this.renderer.domElement.style.cursor = 'default';
6493
+ }
6494
+ }
6495
+ /**
6496
+ * Shows a translucent tag under the cursor while placing, so the spot is visible before commiting.
6497
+ *
6498
+ * Matterport does this with a tag that follows the pointer; the equivalent here is a preview
6499
+ * sprite that gets removed the moment the real tag takes its place.
6500
+ */
6501
+ updatePlacementPreview(event) {
6502
+ const placement = this.pickPlacementOnModel(event);
6503
+ if (!placement) {
6504
+ this.removePlacementPreview();
6505
+ this.renderer.domElement.style.cursor = 'not-allowed';
6506
+ return;
6507
+ }
6508
+ this.renderer.domElement.style.cursor = 'crosshair';
6509
+ if (!this.placementPreview) {
6510
+ this.placementPreview = new THREE.Sprite(new THREE.SpriteMaterial({
6511
+ map: this.createTagTexture('#9E9E9E'),
6512
+ depthTest: false,
6513
+ transparent: true,
6514
+ opacity: 0.6,
6515
+ }));
6516
+ this.placementPreview.renderOrder = 999;
6517
+ this.tagGroup.add(this.placementPreview);
6518
+ this.scaleTagForModel(this.placementPreview);
6519
+ }
6520
+ this.placementPreview.position.set(placement.position.x, placement.position.y, placement.position.z);
6521
+ }
6522
+ removePlacementPreview() {
6523
+ if (!this.placementPreview) {
6524
+ return;
6525
+ }
6526
+ this.tagGroup.remove(this.placementPreview);
6527
+ this.placementPreview.material.map?.dispose();
6528
+ this.placementPreview.material.dispose();
6529
+ this.placementPreview = undefined;
6530
+ }
6531
+ /** Shows or hides one tag, leaving it registered so it can be brought back without reloading. */
6532
+ setTagVisibility(elementId, visible) {
6533
+ const sprite = this.tagSprites.get(elementId);
6534
+ if (sprite) {
6535
+ sprite.visible = visible;
6536
+ }
6537
+ }
6538
+ /**
6539
+ * Shows only tags of the given types, hiding the rest.
6540
+ *
6541
+ * Passing null shows everything - the "no filter" state, distinct from an empty array, which is a
6542
+ * filter that happens to match nothing.
6543
+ */
6544
+ setVisibleTagTypes(types) {
6545
+ this.tagSprites.forEach((sprite) => {
6546
+ sprite.visible = types === null || types.includes(sprite.userData.poiType);
6547
+ });
6548
+ }
6549
+ /**
6550
+ * Moves the camera to look at a tag, mirroring Matterport's goToTag.
6551
+ *
6552
+ * Animated rather than teleported: jumping the camera loses the viewer's sense of where they were
6553
+ * relative to the model. The approach distance follows the model's size so it frames sensibly
6554
+ * whatever the units are.
6555
+ */
6556
+ goToTag(elementId) {
6557
+ const sprite = this.tagSprites.get(elementId);
6558
+ if (!sprite || !this.camera) {
6559
+ return;
6560
+ }
6561
+ const target = sprite.getWorldPosition(new THREE.Vector3());
6562
+ const distance = this.modelRoot
6563
+ ? new THREE.Box3().setFromObject(this.modelRoot).getSize(new THREE.Vector3()).length() * 0.15
6564
+ : 5;
6565
+ // Approach along the current viewing direction, so the camera closes in rather than swinging
6566
+ // around to an arbitrary side.
6567
+ const direction = this.camera.position.clone().sub(this.controls.target).normalize();
6568
+ this.cameraTween = {
6569
+ fromPosition: this.camera.position.clone(),
6570
+ toPosition: target.clone().add(direction.multiplyScalar(distance)),
6571
+ fromTarget: this.controls.target.clone(),
6572
+ toTarget: target,
6573
+ startedAt: performance.now(),
6574
+ durationMs: 600,
6575
+ };
6576
+ }
6577
+ /** Advances the camera animation started by goToTag, if one is running. */
6578
+ stepCameraTween() {
6579
+ if (!this.cameraTween) {
6580
+ return;
6581
+ }
6582
+ const { fromPosition, toPosition, fromTarget, toTarget, startedAt, durationMs } = this.cameraTween;
6583
+ const linear = Math.min((performance.now() - startedAt) / durationMs, 1);
6584
+ // Ease out, so the move settles instead of stopping dead.
6585
+ const t = 1 - Math.pow(1 - linear, 3);
6586
+ this.camera.position.lerpVectors(fromPosition, toPosition, t);
6587
+ this.controls.target.lerpVectors(fromTarget, toTarget, t);
6588
+ if (linear >= 1) {
6589
+ this.cameraTween = null;
6590
+ }
6591
+ }
6592
+ isPlacingTag() {
6593
+ return this.placingTag;
6594
+ }
6595
+ /** The spot picked by the last placement, for whoever commits it to a POI. */
6596
+ getPendingTagPlacement() {
6597
+ return this.pendingTagPlacement;
6598
+ }
6599
+ /**
6600
+ * Turns a click into a point on the model, in model space.
6601
+ *
6602
+ * Returns null when the click missed the geometry, which is a normal outcome - the user clicked
6603
+ * the sky - and leaves placement mode armed so they can simply try again.
6604
+ */
6605
+ pickPlacementOnModel(event) {
6606
+ if (!this.modelRoot) {
6607
+ return null;
6608
+ }
6609
+ const rect = this.renderer.domElement.getBoundingClientRect();
6610
+ this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
6611
+ this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
6612
+ this.raycaster.setFromCamera(this.mouse, this.camera);
6613
+ const hit = this.raycaster.intersectObject(this.modelRoot, true)[0];
6614
+ if (!hit) {
6615
+ return null;
6616
+ }
6617
+ // The hit arrives in world space. Everything persisted has to be in the model's own space, so
6618
+ // it survives the centering and scaling applied at load - and any future re-export.
6619
+ const position = this.modelRoot.worldToLocal(hit.point.clone());
6620
+ // A face normal is in the geometry's local space already, but the mesh it belongs to may sit
6621
+ // under transforms of its own, so bring it back through those to match the position's frame.
6622
+ let normal = new THREE.Vector3(0, 1, 0);
6623
+ if (hit.face) {
6624
+ normal = hit.face.normal
6625
+ .clone()
6626
+ .transformDirection(hit.object.matrixWorld)
6627
+ .transformDirection(new THREE.Matrix4().copy(this.modelRoot.matrixWorld).invert())
6628
+ .normalize();
6629
+ }
6630
+ return {
6631
+ position: { x: position.x, y: position.y, z: position.z },
6632
+ normal: { x: normal.x, y: normal.y, z: normal.z },
6633
+ };
6634
+ }
6635
+ /**
6636
+ * Sets the transform mapping captured tag coordinates onto this model's space.
6637
+ *
6638
+ * Every tag comes from the same source space, so this is one transform on the group rather than
6639
+ * per-tag maths - which also means it applies retroactively to tags already placed, and can be
6640
+ * adjusted live while someone dials the numbers in.
6641
+ *
6642
+ * Passing null resets to identity, i.e. tag coordinates are already in model space.
6643
+ */
6644
+ setTagAlignment(alignment) {
6645
+ const { translation, rotation, scale } = alignment ?? {};
6646
+ this.tagGroup.position.set(translation?.x ?? 0, translation?.y ?? 0, translation?.z ?? 0);
6647
+ this.tagGroup.rotation.set(rotation?.x ?? 0, rotation?.y ?? 0, rotation?.z ?? 0);
6648
+ this.tagGroup.scale.setScalar(scale ?? 1);
6649
+ // Tag size is derived from the world scale it inherits, which this call just changed.
6650
+ this.tagSprites.forEach((sprite) => this.scaleTagForModel(sprite));
6651
+ }
6652
+ /**
6653
+ * Adds a tag, or moves/restyles the one already registered for this element.
6654
+ *
6655
+ * Upsert rather than a declarative "here is the full list" call, because tags arrive per type
6656
+ * (equipment, then tickets, then measurements...) from separate subscriptions - replacing the set
6657
+ * on each of those would wipe the types that came before.
6658
+ */
6659
+ upsertTag(tag) {
6660
+ const existing = this.tagSprites.get(tag.id);
6661
+ if (existing) {
6662
+ existing.position.set(tag.position.x, tag.position.y, tag.position.z);
6663
+ existing.userData.poiType = tag.poiType ?? existing.userData.poiType;
6664
+ return;
6665
+ }
6666
+ const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
6667
+ map: this.createTagTexture(tag.color),
6668
+ // Tags read as UI rather than geometry, so they stay visible through the model instead of
6669
+ // being occluded by whatever wall happens to be between them and the camera.
6670
+ depthTest: false,
6671
+ transparent: true,
6672
+ }));
6673
+ sprite.name = tag.id;
6674
+ sprite.userData.poiType = tag.poiType ?? null;
6675
+ sprite.position.set(tag.position.x, tag.position.y, tag.position.z);
6676
+ sprite.renderOrder = 999;
6677
+ this.scaleTagForModel(sprite);
6678
+ this.tagGroup.add(sprite);
6679
+ this.tagSprites.set(tag.id, sprite);
6680
+ if (tag.iconUrl) {
6681
+ this.applyTagIcon(sprite, tag.iconUrl);
6682
+ }
6683
+ }
6684
+ /** Removes the tag registered for an element, if there is one. */
6685
+ removeTag(elementId) {
6686
+ const sprite = this.tagSprites.get(elementId);
6687
+ if (!sprite) {
6688
+ return;
6689
+ }
6690
+ this.tagGroup.remove(sprite);
6691
+ sprite.material.map?.dispose();
6692
+ sprite.material.dispose();
6693
+ this.tagSprites.delete(elementId);
6694
+ }
6695
+ /** Removes every tag from the scene. */
6696
+ clearTags() {
6697
+ [...this.tagSprites.keys()].forEach((id) => this.removeTag(id));
6698
+ }
6699
+ /** Loads an icon into an existing tag, leaving the fallback disc in place if it fails. */
6700
+ applyTagIcon(sprite, iconUrl) {
6701
+ new THREE.TextureLoader().load(iconUrl, (texture) => {
6702
+ sprite.material.map?.dispose();
6703
+ sprite.material.map = texture;
6704
+ sprite.material.needsUpdate = true;
6705
+ }, undefined, () => console.warn(`ThreeJsService: could not load tag icon "${iconUrl}", keeping the default marker.`));
6706
+ }
6707
+ /** Draws the default tag marker: a filled disc with a white ring, so it reads on any background. */
6708
+ createTagTexture(color = '#1E88E5') {
6709
+ const size = 128;
6710
+ const canvas = document.createElement('canvas');
6711
+ canvas.width = size;
6712
+ canvas.height = size;
6713
+ const context = canvas.getContext('2d');
6714
+ context.beginPath();
6715
+ context.arc(size / 2, size / 2, size / 2 - 8, 0, Math.PI * 2);
6716
+ context.fillStyle = color;
6717
+ context.fill();
6718
+ context.lineWidth = 8;
6719
+ context.strokeStyle = '#FFFFFF';
6720
+ context.stroke();
6721
+ return new THREE.CanvasTexture(canvas);
6722
+ }
6723
+ /**
6724
+ * Sizes a tag relative to the model.
6725
+ *
6726
+ * A sprite inherits the model's scale and the alignment's, so a fixed size would come out tiny on
6727
+ * a large model and huge on a small one. Dividing by the group's world scale keeps tags the same
6728
+ * size on screen whatever the model's units are.
6729
+ */
6730
+ scaleTagForModel(sprite) {
6731
+ const worldScale = this.tagGroup.getWorldScale(new THREE.Vector3()).x || 1;
6732
+ const size = 0.4 / worldScale;
6733
+ sprite.scale.set(size, size, 1);
6734
+ }
6735
+ /**
6736
+ * Adds secondary interactive objects (e.g., POIs, sensors) to the scene.
6737
+ */
6738
+ updateObjsToScene(objsAdded) {
6739
+ const existingIds = this.clickableObjects.map((obj) => obj.name);
6740
+ const objLoader = new OBJLoader();
6741
+ // delete objects that are not in the provided array
6742
+ this.clickableObjects.forEach((obj) => {
6743
+ if (!objsAdded.find((objData) => objData.id === obj.name)) {
6744
+ this.scene.remove(obj);
6745
+ this.clickableObjects = this.clickableObjects.filter((object) => object.name !== obj.name);
6746
+ }
6747
+ });
6748
+ objsAdded.forEach((objectData) => {
6749
+ if (!existingIds.includes(objectData.id)) {
6750
+ objLoader.load(objectData.file, (newObject) => {
6751
+ newObject.name = objectData.id;
6752
+ newObject.traverse((child) => {
6753
+ if (child.isMesh) {
6754
+ const mesh = child;
6755
+ if (mesh.material instanceof THREE.Material) {
6756
+ mesh.material.transparent = true;
6757
+ mesh.material.opacity = 0.8;
6758
+ }
6759
+ }
6760
+ });
6761
+ // set object position and rotation
6762
+ newObject.position.set(objectData.position.x, objectData.position.y, objectData.position.z);
6763
+ // The JSON configuration has rotation already in radians (e.g. 3.1415...)
6764
+ newObject.rotation.set(objectData.rotation.x, objectData.rotation.y, objectData.rotation.z);
6765
+ newObject.scale.set(objectData.scale.x, objectData.scale.y, objectData.scale.z);
6766
+ this.scene.add(newObject);
6767
+ this.clickableObjects.push(newObject);
6768
+ });
6769
+ }
6770
+ });
6771
+ }
6772
+ onMouseMove(event) {
6773
+ if (!this.isDraggingCamera) {
6774
+ const rect = this.renderer.domElement.getBoundingClientRect();
6775
+ this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
6776
+ this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
6777
+ this.raycaster.setFromCamera(this.mouse, this.camera);
6778
+ // While placing, the preview follows the pointer and nothing else should react to it.
6779
+ if (this.placingTag) {
6780
+ this.updatePlacementPreview(event);
6781
+ return;
6782
+ }
6783
+ // Tags first: they draw on top of the model, so they have to answer the pointer first too.
6784
+ if (this.raycaster.intersectObjects(this.tagGroup.children, false).some((hit) => hit.object.visible)) {
6785
+ this.renderer.domElement.style.cursor = 'pointer';
6786
+ this.objectHovered$.next(null);
6787
+ return;
6788
+ }
6789
+ const intersects = this.raycaster.intersectObjects(this.clickableObjects, true);
6790
+ if (intersects.length > 0) {
6791
+ this.renderer.domElement.style.cursor = 'pointer';
6792
+ const intersectedObjectId = intersects[0].object?.parent?.name;
6793
+ if (intersectedObjectId) {
6794
+ this.objectHovered$.next({ id: intersectedObjectId, event });
6795
+ }
6796
+ }
6797
+ else {
6798
+ this.renderer.domElement.style.cursor = 'default';
6799
+ this.objectHovered$.next(null);
6800
+ }
6801
+ }
6802
+ else {
6803
+ this.renderer.domElement.style.cursor = 'grabbing';
6804
+ this.objectHovered$.next(null);
6805
+ }
6806
+ }
6807
+ onMouseClick(event) {
6808
+ if (!this.isDraggingCamera) {
6809
+ const rect = this.renderer.domElement.getBoundingClientRect();
6810
+ this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
6811
+ this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
6812
+ // Alignment and placement both take the click before anything can be opened by it.
6813
+ if (this.aligning) {
6814
+ this.raycaster.setFromCamera(this.mouse, this.camera);
6815
+ this.handleAlignmentClick(event);
6816
+ return;
6817
+ }
6818
+ // Placement wins over everything: while armed, a click means "put it here", not "open that".
6819
+ if (this.placingTag) {
6820
+ const placement = this.pickPlacementOnModel(event);
6821
+ if (placement) {
6822
+ this.pendingTagPlacement = placement;
6823
+ this.placingTag = false;
6824
+ this.removePlacementPreview();
6825
+ this.renderer.domElement.style.cursor = 'default';
6826
+ this.tagPlaced$.next(placement);
6827
+ }
6828
+ return;
6829
+ }
6830
+ this.raycaster.setFromCamera(this.mouse, this.camera);
6831
+ // Same precedence as hover: a tag drawn over the model claims the click.
6832
+ const tagHits = this.raycaster.intersectObjects(this.tagGroup.children, false)
6833
+ .filter((hit) => hit.object.visible);
6834
+ if (tagHits.length > 0) {
6835
+ const tag = tagHits[0].object;
6836
+ this.tagClicked$.next({ elementId: tag.name, poiType: tag.userData.poiType ?? null });
6837
+ return;
6838
+ }
6839
+ const intersects = this.raycaster.intersectObjects(this.clickableObjects, true);
6840
+ if (intersects.length > 0) {
6841
+ const clickedObject = intersects[0].object;
6842
+ const id = clickedObject?.parent?.name;
6843
+ if (id) {
6844
+ this.objectClicked$.next(id);
6845
+ }
6846
+ }
6847
+ else {
6848
+ this.objectClicked$.next(null);
6849
+ }
6850
+ }
6851
+ }
6852
+ animate = () => {
6853
+ this.animationFrameId = requestAnimationFrame(this.animate);
6854
+ this.stepCameraTween();
6855
+ this.controls.update();
6856
+ this.renderer.render(this.scene, this.camera);
6857
+ };
6858
+ destroy() {
6859
+ if (this.animationFrameId) {
6860
+ cancelAnimationFrame(this.animationFrameId);
6861
+ }
6862
+ if (this.canvasResizeObserver) {
6863
+ this.canvasResizeObserver.disconnect();
6864
+ this.canvasResizeObserver = undefined;
6865
+ }
6866
+ this.discardCurrentLoad();
6867
+ this.clearTags();
6868
+ if (this.renderer) {
6869
+ this.renderer.dispose();
6870
+ }
6871
+ if (this.controls) {
6872
+ this.controls.dispose();
6873
+ }
6874
+ }
6875
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ThreeJsService, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
6876
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ThreeJsService, providedIn: 'root' });
6877
+ }
6878
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ThreeJsService, decorators: [{
6879
+ type: Injectable,
6880
+ args: [{
6881
+ providedIn: 'root'
6882
+ }]
6883
+ }], ctorParameters: () => [{ type: i0.NgZone }] });
6884
+
6885
+ /* eslint-disable no-await-in-loop */
6886
+ /* eslint-disable camelcase */
6887
+ /* eslint-disable no-underscore-dangle */
6888
+ /* eslint-disable class-methods-use-this */
6889
+ class ViewerService {
6890
+ sdkKey;
6891
+ poiService;
6892
+ matterportService;
6893
+ threeJsService;
6894
+ viewerMode = 'Matterport';
6895
+ currentModel3D;
6896
+ tourUrl;
6897
+ viewerIsOn = new Subject();
6898
+ dataIsLoaded = new Subject();
6899
+ isLoaded = false;
6900
+ showingViewer = false;
6901
+ sweepToMove;
6902
+ rotationToMove;
6903
+ goToTriggered = new Subject();
6904
+ toolboxStates = {
6905
+ dollhouse: {
6906
+ stat: false,
6907
+ iconId: 'ui-dollhouse',
6908
+ show() {
6909
+ },
6910
+ hide() {
6911
+ },
6912
+ },
6913
+ floorplan: {
6914
+ stat: false,
6915
+ },
6916
+ mesure: {
6917
+ stat: false,
6918
+ iconId: 'ui-mesure',
6919
+ show() {
6920
+ },
6921
+ hide() {
6922
+ },
6923
+ },
6924
+ filter: {
6925
+ stat: false,
6926
+ },
6927
+ };
6928
+ isObjectLoaded = false;
6929
+ constructor(sdkKey, poiService, matterportService, threeJsService) {
6930
+ this.sdkKey = sdkKey;
6931
+ this.poiService = poiService;
6932
+ this.matterportService = matterportService;
6933
+ this.threeJsService = threeJsService;
6934
+ this.setMode('Matterport');
6935
+ }
6936
+ /*-----------------------------------------------------------------------------------------------------------------
6937
+ Utils
6938
+ -----------------------------------------------------------------------------------------------------------------*/
6939
+ checkMode(indexMode) {
6940
+ return this.viewerMode === indexMode;
5442
6941
  }
5443
6942
  /*-----------------------------------------------------------------------------------------------------------------
5444
6943
  Setters
5445
6944
  -----------------------------------------------------------------------------------------------------------------*/
5446
6945
  setMode(indexMode) {
6946
+ // Leaving Three.js means no Three.js work should still be running. The service behind it is a
6947
+ // singleton and outlives the component, so a download started for a model the user has navigated
6948
+ // away from would otherwise finish - and its parse would hold the main thread for seconds,
6949
+ // stalling whatever viewer they actually opened.
6950
+ if (this.viewerMode === 'threejs' && indexMode !== 'threejs') {
6951
+ this.threeJsService.destroy();
6952
+ }
5447
6953
  this.viewerMode = indexMode;
5448
6954
  }
5449
6955
  setTourUrl(options) {
@@ -5503,7 +7009,10 @@ class ViewerService {
5503
7009
  async clearAll() {
5504
7010
  // this.sweepToMove = null;
5505
7011
  // this.rotationToMove = null;
5506
- if (this.checkMode('Matterport')) {
7012
+ if (this.checkMode('threejs')) {
7013
+ this.threeJsService.destroy();
7014
+ }
7015
+ else if (this.checkMode('Matterport')) {
5507
7016
  this.showingViewer = false;
5508
7017
  this.isLoaded = false;
5509
7018
  await this.matterportService.clearAll();
@@ -5581,15 +7090,136 @@ class ViewerService {
5581
7090
  if (this.checkMode('Matterport')) {
5582
7091
  return this.matterportService.createMattertagFromPOI(poi.type, object, poi);
5583
7092
  }
7093
+ if (this.checkMode('threejs')) {
7094
+ return this.addThreeJsTagFromPOI(object, poi);
7095
+ }
5584
7096
  // Cupix
5585
7097
  // TODO
5586
7098
  return Promise.resolve();
5587
7099
  }
7100
+ /**
7101
+ * Draws a POI as a tag in the Three.js scene.
7102
+ *
7103
+ * Both viewers are fed by the same subscriptions on the same POI records, so a tag created in
7104
+ * either one shows up in the other without any syncing of its own - this is just the Three.js end
7105
+ * of the pipeline that already drives Matterport.
7106
+ */
7107
+ async addThreeJsTagFromPOI(object, poi) {
7108
+ if (!poi.coordinate) {
7109
+ return;
7110
+ }
7111
+ let position;
7112
+ try {
7113
+ position = JSON.parse(poi.coordinate);
7114
+ }
7115
+ catch {
7116
+ console.warn(`ViewerService: unparseable POI coordinate for element ${object.id}:`, poi.coordinate);
7117
+ return;
7118
+ }
7119
+ if (![position?.x, position?.y, position?.z].every((value) => typeof value === 'number')) {
7120
+ return;
7121
+ }
7122
+ this.threeJsService.upsertTag({
7123
+ id: object.id,
7124
+ position,
7125
+ poiType: poi.type,
7126
+ iconUrl: poi.tagIcon ? await getSignedFile(poi.tagIcon) : null,
7127
+ });
7128
+ }
7129
+ /** Moves the Three.js camera to frame a tag, mirroring Matterport's goToTag. */
7130
+ goToThreeJsTag(elementId) {
7131
+ if (this.checkMode('threejs')) {
7132
+ this.threeJsService.goToTag(elementId);
7133
+ }
7134
+ }
7135
+ /** Shows only tags of the given POI types; null clears the filter. */
7136
+ setVisibleThreeJsTagTypes(types) {
7137
+ if (this.checkMode('threejs')) {
7138
+ this.threeJsService.setVisibleTagTypes(types);
7139
+ }
7140
+ }
7141
+ /**
7142
+ * Alignment walkthrough: pick a tag, then click where it really sits, three times over.
7143
+ *
7144
+ * Uses tags that already exist, so nothing is created and nothing is written until the caller
7145
+ * saves the result. See threeJsAlignmentProgress$ for what the UI should be asking for.
7146
+ */
7147
+ startThreeJsTagAlignment() {
7148
+ if (this.checkMode('threejs')) {
7149
+ this.threeJsService.startTagAlignment();
7150
+ }
7151
+ }
7152
+ cancelThreeJsTagAlignment() {
7153
+ if (this.checkMode('threejs')) {
7154
+ this.threeJsService.cancelTagAlignment();
7155
+ }
7156
+ }
7157
+ get threeJsAlignmentProgress$() {
7158
+ return this.threeJsService.alignmentProgress$;
7159
+ }
7160
+ /**
7161
+ * Solves the alignment and applies it to the scene, returning it as the JSON to store in the
7162
+ * capture's metadata. Null means the three tags were collinear and nothing was applied.
7163
+ */
7164
+ solveThreeJsTagAlignment(existingMetadata) {
7165
+ if (!this.checkMode('threejs')) {
7166
+ return null;
7167
+ }
7168
+ const alignment = this.threeJsService.solveAlignmentFromPairs();
7169
+ if (!alignment) {
7170
+ return null;
7171
+ }
7172
+ // Merged into whatever the capture already stores, so an existing camera placement survives.
7173
+ let stored = {};
7174
+ try {
7175
+ stored = existingMetadata ? JSON.parse(existingMetadata) : {};
7176
+ }
7177
+ catch {
7178
+ stored = {};
7179
+ }
7180
+ stored.alignment = alignment;
7181
+ return { alignment, metadata: JSON.stringify(stored) };
7182
+ }
7183
+ /** Shows or hides a single Three.js tag. */
7184
+ setThreeJsTagVisibility(elementId, visible) {
7185
+ if (this.checkMode('threejs')) {
7186
+ this.threeJsService.setTagVisibility(elementId, visible);
7187
+ }
7188
+ }
5588
7189
  async action_delete_element(element) {
5589
7190
  // Matterport
5590
7191
  if (this.checkMode('Matterport')) {
5591
7192
  await this.matterportService.deleteMattertagForObject(element.id);
5592
7193
  }
7194
+ if (this.checkMode('threejs')) {
7195
+ this.threeJsService.removeTag(element.id);
7196
+ }
7197
+ }
7198
+ /** Emits when a Three.js model load actually stopped short, rather than merely being asked to. */
7199
+ get threeJsModelLoadCancelled$() {
7200
+ return this.threeJsService.modelLoadCancelled$;
7201
+ }
7202
+ /** Stops the Three.js model download in progress. */
7203
+ cancelThreeJsModelLoad() {
7204
+ if (this.checkMode('threejs')) {
7205
+ this.threeJsService.cancelModelLoad();
7206
+ }
7207
+ }
7208
+ /** Emits the element id of a tag clicked in the Three.js scene. */
7209
+ get threeJsTagClicked$() {
7210
+ return this.threeJsService.tagClicked$;
7211
+ }
7212
+ /**
7213
+ * Applies the visit's stored tag alignment, mapping captured tag coordinates onto this model.
7214
+ *
7215
+ * Safe to call before the model or any tag exists: it is a transform on the group both end up
7216
+ * under, so it applies to whatever arrives later.
7217
+ */
7218
+ setThreeJsTagAlignmentFromMetadata(metadata) {
7219
+ if (!this.checkMode('threejs')) {
7220
+ return;
7221
+ }
7222
+ this.threeJsService.setTagAlignment(parseThreeJsPlacement(metadata)?.alignment ?? null);
5593
7223
  }
5594
7224
  /**
5595
7225
  * Enable tag placement menu to add a new element depending on its type (TICKET, DATA, EQUIPMENT etc)
@@ -5601,6 +7231,9 @@ class ViewerService {
5601
7231
  this.matterportService.getInteractionMode() === ViewerInteractions.DEFAULT) {
5602
7232
  await this.matterportService.addMattertagWhenAdding(poiType);
5603
7233
  }
7234
+ if (this.checkMode('threejs')) {
7235
+ this.threeJsService.startTagPlacement();
7236
+ }
5604
7237
  }
5605
7238
  /**
5606
7239
  * Launches the repositioning of an existing Mattertag related to parameter object
@@ -5621,6 +7254,9 @@ class ViewerService {
5621
7254
  if (this.checkMode('Matterport')) {
5622
7255
  this.matterportService.cancelFollowingCursor();
5623
7256
  }
7257
+ if (this.checkMode('threejs')) {
7258
+ this.threeJsService.cancelTagPlacement();
7259
+ }
5624
7260
  }
5625
7261
  action_toolbox_inside_view() {
5626
7262
  // Matterport
@@ -5704,8 +7340,51 @@ class ViewerService {
5704
7340
  this.matterportService.deleteLastMattertag();
5705
7341
  return Promise.resolve(poi);
5706
7342
  }
7343
+ if (this.checkMode('threejs')) {
7344
+ return this.commitThreeJsTagPlacement(poi, isValid, element);
7345
+ }
5707
7346
  return Promise.resolve(poi);
5708
7347
  }
7348
+ /**
7349
+ * Writes the spot picked in the Three.js scene into the POI, mirroring the Matterport branch.
7350
+ *
7351
+ * The coordinate goes in the same field and the same space Matterport writes, which is what makes
7352
+ * a tag created here show up in the right place there - and the other way round.
7353
+ */
7354
+ async commitThreeJsTagPlacement(poi, isValid, element) {
7355
+ const placement = this.threeJsService.getPendingTagPlacement();
7356
+ if (!isValid || !placement || !poi) {
7357
+ this.threeJsService.cancelTagPlacement();
7358
+ return Promise.resolve(poi);
7359
+ }
7360
+ const poiMetadata = poi.metadata ? JSON.parse(poi.metadata) : {};
7361
+ poiMetadata.normal = placement.normal;
7362
+ const updatedPoi = await this.poiService.updatePoi({
7363
+ id: poi.id,
7364
+ coordinate: JSON.stringify(placement.position),
7365
+ metadata: JSON.stringify(poiMetadata),
7366
+ // matterportSweepID is left as it was: this viewer has no sweeps, and Matterport filters tag
7367
+ // visibility by them, so clearing it would hide the tag over there.
7368
+ });
7369
+ this.threeJsService.upsertTag({
7370
+ id: element.id,
7371
+ position: placement.position,
7372
+ poiType: updatedPoi?.type ?? poi.type,
7373
+ iconUrl: updatedPoi?.tagIcon ? await getSignedFile(updatedPoi.tagIcon) : null,
7374
+ });
7375
+ this.threeJsService.cancelTagPlacement();
7376
+ return updatedPoi;
7377
+ }
7378
+ /** Emits when a tag is dropped on the model in the Three.js scene. */
7379
+ get threeJsTagPlaced$() {
7380
+ return this.threeJsService.tagPlaced$;
7381
+ }
7382
+ /** Leaves Three.js tag placement mode without placing anything. */
7383
+ cancelThreeJsTagPlacement() {
7384
+ if (this.checkMode('threejs')) {
7385
+ this.threeJsService.cancelTagPlacement();
7386
+ }
7387
+ }
5709
7388
  editOnlyTagStyleForPoi(poi, object) {
5710
7389
  if (this.checkMode('Matterport')) {
5711
7390
  const tagData = this.matterportService.getTagFromElementId(poi.elementID);
@@ -5757,6 +7436,79 @@ class ViewerService {
5757
7436
  https://matterport.github.io/showcase-sdk/docs/sdk/reference/current/modules/mattertag.html#add
5758
7437
  https://matterport.github.io/showcase-sdk/docs/sdk/reference/current/modules/mattertag.html#remove
5759
7438
  -----------------------------------------------------------------------------------------------------------------*/
7439
+ initThreeJs(canvas) {
7440
+ if (this.checkMode('threejs')) {
7441
+ this.threeJsService.init(canvas);
7442
+ }
7443
+ }
7444
+ /**
7445
+ * Loads a 3D model into the Three.js scene (GLTF).
7446
+ * Only works when the viewer is in 'threejs' mode.
7447
+ */
7448
+ loadGLTFModel(url, cameraPosition, rotation) {
7449
+ if (this.checkMode('threejs')) {
7450
+ this.threeJsService.loadGLTFModel(url, cameraPosition, rotation);
7451
+ }
7452
+ }
7453
+ /**
7454
+ * Loads a GLB stored in S3, given its key (e.g. `visits/<id>/model.glb`).
7455
+ *
7456
+ * Resolves the key to a signed URL first, then loads it like any other. The signed URL only
7457
+ * covers authentication - the bucket still needs a CORS rule allowing GET from the app's origin,
7458
+ * or the browser blocks the download before it starts.
7459
+ *
7460
+ * `placementMetadata` is the capture's stored placement; when absent or unusable the viewer falls
7461
+ * back to framing the model automatically.
7462
+ */
7463
+ async loadGLTFModelFromS3(key, placementMetadata) {
7464
+ if (!this.checkMode('threejs')) {
7465
+ return;
7466
+ }
7467
+ // A key under assets/ is served by the app itself, so it needs no signing. This is what lets a
7468
+ // model bundled with the build stand in for one in S3 while the two are otherwise identical.
7469
+ const isLocalAsset = key.startsWith('assets/');
7470
+ const url = isLocalAsset ? key : await getSignedFile(key);
7471
+ if (!url) {
7472
+ console.error(`ViewerService: could not resolve a signed URL for the 3D model at "${key}".`);
7473
+ return;
7474
+ }
7475
+ const placement = parseThreeJsPlacement(placementMetadata);
7476
+ this.threeJsService.loadGLTFModel(url, placement?.cameraPosition, placement?.rotation);
7477
+ }
7478
+ /**
7479
+ * Loads a 3D model into the Three.js scene (OBJ).
7480
+ */
7481
+ loadObjModel(mtlUrl, objUrl, cameraPosition, rotation) {
7482
+ if (this.checkMode('threejs')) {
7483
+ this.threeJsService.loadObjModel(mtlUrl, objUrl, cameraPosition, rotation);
7484
+ }
7485
+ }
7486
+ get threeJsObjectHovered$() {
7487
+ return this.threeJsService.objectHovered$;
7488
+ }
7489
+ get threeJsObjectClicked$() {
7490
+ return this.threeJsService.objectClicked$;
7491
+ }
7492
+ /** Emits true while a heavy .obj file is being parsed (in a worker, when available), false once done. */
7493
+ get threeJsModelLoading$() {
7494
+ return this.threeJsService.modelLoading$;
7495
+ }
7496
+ /** Download progress for GLB models as a 0-1 fraction, or null when the size isn't known upfront. */
7497
+ get threeJsModelLoadProgress$() {
7498
+ return this.threeJsService.modelLoadProgress$;
7499
+ }
7500
+ /**
7501
+ * Registers the Web Worker used to parse heavy .obj files off the main thread.
7502
+ * Must be created by the app (see obj-parser.worker.ts), since this library can't bundle worker files.
7503
+ */
7504
+ setObjParserWorker(worker) {
7505
+ this.threeJsService.setObjParserWorker(worker);
7506
+ }
7507
+ addThreeJsObjects(objs) {
7508
+ if (this.checkMode('threejs')) {
7509
+ this.threeJsService.updateObjsToScene(objs);
7510
+ }
7511
+ }
5760
7512
  async initMatterportSdk(spaceID, model3D, tagService, module = SpModule.IMMO) {
5761
7513
  this.isLoaded = false;
5762
7514
  this.matterportService.currentSpaceID = spaceID;
@@ -5816,235 +7568,105 @@ class ViewerService {
5816
7568
  isObject3DLoadedInPosition() {
5817
7569
  return this.isObjectLoaded;
5818
7570
  }
5819
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, deps: [{ token: MATTERPORT_SDK_KEY }, { token: PoiService }, { token: MatterportService }], target: i0.ɵɵFactoryTarget.Injectable });
7571
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, deps: [{ token: MATTERPORT_SDK_KEY }, { token: PoiService }, { token: MatterportService }, { token: ThreeJsService }], target: i0.ɵɵFactoryTarget.Injectable });
5820
7572
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, providedIn: 'root' });
5821
- }
5822
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, decorators: [{
5823
- type: Injectable,
5824
- args: [{
5825
- providedIn: 'root',
5826
- }]
5827
- }], ctorParameters: () => [{ type: undefined, decorators: [{
5828
- type: Inject,
5829
- args: [MATTERPORT_SDK_KEY]
5830
- }] }, { type: PoiService }, { type: MatterportService }] });
5831
-
5832
- class ModalSwitchVisitComponent {
5833
- activeModal;
5834
- router;
5835
- viewerService;
5836
- content;
5837
- model3D;
5838
- spaceID;
5839
- dataToMove;
5840
- constructor(activeModal, router, viewerService) {
5841
- this.activeModal = activeModal;
5842
- this.router = router;
5843
- this.viewerService = viewerService;
5844
- }
5845
- ngOnInit() { }
5846
- switchVisit() {
5847
- this.activeModal.close();
5848
- if (this.dataToMove) {
5849
- this.viewerService.setSweepToMove(this.dataToMove.sweepToMove);
5850
- this.viewerService.setRotationToMove(this.dataToMove.rotation);
5851
- }
5852
- // this.objectSubService.forceRemove = true; // we reset the tags when changing visit
5853
- this.router.navigate(["visit", this.spaceID], {
5854
- queryParams: { model3D: this.model3D },
5855
- });
5856
- }
5857
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ModalSwitchVisitComponent, deps: [{ token: i1$2.NgbActiveModal }, { token: i1$1.Router }, { token: ViewerService }], target: i0.ɵɵFactoryTarget.Component });
5858
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: ModalSwitchVisitComponent, selector: "lib-modal-switch-visit", inputs: { content: "content", model3D: "model3D", spaceID: "spaceID", dataToMove: "dataToMove" }, ngImport: i0, template: "<div class=\"modal-container\">\r\n <p>{{content}}</p>\r\n <button class=\"btn btn-outline-primary rounded-pill\" (click)=\"switchVisit()\">{{'Yes, switch visit' | translate }}</button>\r\n <button class=\"btn btn-outline-primary rounded-pill\"(click)=\"activeModal.close()\">{{'No, stay here' | translate }}</button>\r\n</div>\r\n", styles: [".modal-container{margin:10px}button{margin-right:8px}\n"], dependencies: [{ kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
5859
- }
5860
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ModalSwitchVisitComponent, decorators: [{
5861
- type: Component,
5862
- args: [{ selector: 'lib-modal-switch-visit', template: "<div class=\"modal-container\">\r\n <p>{{content}}</p>\r\n <button class=\"btn btn-outline-primary rounded-pill\" (click)=\"switchVisit()\">{{'Yes, switch visit' | translate }}</button>\r\n <button class=\"btn btn-outline-primary rounded-pill\"(click)=\"activeModal.close()\">{{'No, stay here' | translate }}</button>\r\n</div>\r\n", styles: [".modal-container{margin:10px}button{margin-right:8px}\n"] }]
5863
- }], ctorParameters: () => [{ type: i1$2.NgbActiveModal }, { type: i1$1.Router }, { type: ViewerService }], propDecorators: { content: [{
5864
- type: Input
5865
- }], model3D: [{
5866
- type: Input
5867
- }], spaceID: [{
5868
- type: Input
5869
- }], dataToMove: [{
5870
- type: Input
5871
- }] } });
5872
-
5873
- const deleteFromS3 = async (path) => {
5874
- await Storage.remove(path, { level: "public" })
5875
- .then((result) => console.log("Successfully deleted =>", result))
5876
- .catch((error) => console.log("Deleting from S3 error:", error));
5877
- };
5878
- const uploadFileToS3 = async (path, file, objectId) => {
5879
- const prefix = objectId ? `${objectId.slice(0, 8)}_` : "";
5880
- return Storage.put(path + prefix + file.name, file, {
5881
- progressCallback(progress) {
5882
- // console.log('Uploaded : ', progress);
5883
- },
5884
- contentType: file.type,
5885
- })
5886
- .then((result) => {
5887
- console.log("Success =>", result);
5888
- return result.key;
5889
- });
5890
- };
5891
- const getSignedImageUrlForEquipment = async (equip) => {
5892
- if (!equip.photo) {
5893
- return Promise.resolve(null);
5894
- }
5895
- return Storage.get(equip.photo, { level: "public" })
5896
- .then((result) => {
5897
- return result;
5898
- })
5899
- .catch((error) => {
5900
- console.log("error fetching file from s3 =>", error);
5901
- return null;
5902
- });
5903
- };
5904
- const getSignedImageUrlForProfile = async (profile) => {
5905
- return Storage.get(profile.avatar, { level: "public" })
5906
- .then((result) => {
5907
- return result;
5908
- })
5909
- .catch((error) => {
5910
- console.log("error fetching file from s3 =>", error);
5911
- });
5912
- };
5913
- const getSignedImageUrlForSpace = async (space) => {
5914
- return Storage.get(space.annexes, { level: "public" })
5915
- .then((result) => {
5916
- return result;
5917
- })
5918
- .catch((error) => {
5919
- console.log("error fetching file from s3 =>", error);
5920
- });
5921
- };
5922
- const uploadBase64Image = async (uri, objectId, path, friendlyFileName) => {
5923
- return fetch(uri)
5924
- .then((res) => res.blob())
5925
- .then((blob) => {
5926
- const file = new File([blob], `${friendlyFileName}.jpeg`, {
5927
- type: "image/jpeg",
5928
- });
5929
- return uploadFileToS3(path, file, objectId);
5930
- });
5931
- };
5932
- const uploadBase64ImageWithRetry = async (uri, objectId, path, friendlyFileName, retry) => {
5933
- return new Promise(async (resolve, reject) => {
5934
- while (retry > 0) {
5935
- try {
5936
- const r = await uploadBase64Image(uri, objectId, path, friendlyFileName);
5937
- resolve(r);
5938
- return;
5939
- }
5940
- catch (e) {
5941
- retry--;
5942
- console.error(`Upload error, retry n° ${retry} : `, e);
5943
- }
7573
+ }
7574
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, decorators: [{
7575
+ type: Injectable,
7576
+ args: [{
7577
+ providedIn: 'root',
7578
+ }]
7579
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
7580
+ type: Inject,
7581
+ args: [MATTERPORT_SDK_KEY]
7582
+ }] }, { type: PoiService }, { type: MatterportService }, { type: ThreeJsService }] });
7583
+
7584
+ class ModalSwitchVisitComponent {
7585
+ activeModal;
7586
+ router;
7587
+ viewerService;
7588
+ content;
7589
+ model3D;
7590
+ spaceID;
7591
+ dataToMove;
7592
+ constructor(activeModal, router, viewerService) {
7593
+ this.activeModal = activeModal;
7594
+ this.router = router;
7595
+ this.viewerService = viewerService;
7596
+ }
7597
+ ngOnInit() { }
7598
+ switchVisit() {
7599
+ this.activeModal.close();
7600
+ if (this.dataToMove) {
7601
+ this.viewerService.setSweepToMove(this.dataToMove.sweepToMove);
7602
+ this.viewerService.setRotationToMove(this.dataToMove.rotation);
5944
7603
  }
5945
- ;
5946
- reject("Max retry exceeded");
5947
- });
5948
- };
5949
- const removeAllFilesFromFolderS3 = async (folderPath) => {
5950
- const arrayOfPromises = [];
5951
- Storage.list(folderPath, { level: "public" })
5952
- .then((res) => {
5953
- Object.keys(res).map(async (key) => {
5954
- const file = res[key];
5955
- arrayOfPromises.push(deleteFromS3(file.key));
7604
+ // this.objectSubService.forceRemove = true; // we reset the tags when changing visit
7605
+ this.router.navigate(["visit", this.spaceID], {
7606
+ queryParams: { model3D: this.model3D },
5956
7607
  });
5957
- })
5958
- .catch((error) => console.error("List failure", error));
5959
- return Promise.all(arrayOfPromises);
5960
- };
5961
- const listFilesInFolder = async (folderPath) => {
5962
- return Storage.list(folderPath, { level: "public", pageSize: 'ALL' })
5963
- .then((res) => {
5964
- return res.results;
5965
- })
5966
- .catch((error) => console.error("List failure", error));
5967
- };
5968
- const getSignedFile = async (path) => {
5969
- return Storage.get(path, { level: "public" })
5970
- .then((result) => {
5971
- return result;
5972
- })
5973
- .catch((error) => {
5974
- console.log("error fetching file from s3 =>", error);
5975
- return null;
5976
- });
5977
- };
5978
- const downloadFileAsObject = async (path) => {
5979
- return Storage.get(path, { download: true, level: "public" })
5980
- .then((result) => {
5981
- return result;
5982
- })
5983
- .catch((error) => {
5984
- console.log("error fetching file from s3 =>", error);
5985
- });
5986
- };
5987
- const getBufferForFileFromS3 = async (path) => {
5988
- const object = await downloadFileAsObject(path);
5989
- if (object) {
5990
- // @ts-ignore
5991
- return new ArrayBuffer(object.Body, "binary");
5992
7608
  }
5993
- return null;
5994
- };
5995
- const uploadJsonToS3 = async (path, json, filename) => {
5996
- return Storage.put(path + filename, json, {
5997
- contentType: "application/json",
5998
- })
5999
- .then((result) => {
6000
- console.log("Success =>", result);
6001
- return result.key;
6002
- })
6003
- .catch((error) => {
6004
- console.log("error =>", error);
6005
- return null;
6006
- });
6007
- };
6008
- /**
6009
- * Physically duplicates an S3 object to a new key. Used when copying attachments
6010
- * between features/tickets/etc. so the copy is fully independent: deleting the
6011
- * original file later must not affect the copy.
6012
- * IMPORTANT: does NOT swallow errors — callers need the real error (e.g. NoSuchKey)
6013
- * to distinguish an orphaned DB reference from a genuine copy failure.
6014
- */
6015
- const copyFileInS3 = async (sourceKey, destKey) => {
6016
- const result = await Storage.copy({ key: sourceKey, level: "public" }, { key: destKey, level: "public" });
6017
- return result.key;
6018
- };
7609
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ModalSwitchVisitComponent, deps: [{ token: i1$2.NgbActiveModal }, { token: i1$1.Router }, { token: ViewerService }], target: i0.ɵɵFactoryTarget.Component });
7610
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: ModalSwitchVisitComponent, selector: "lib-modal-switch-visit", inputs: { content: "content", model3D: "model3D", spaceID: "spaceID", dataToMove: "dataToMove" }, ngImport: i0, template: "<div class=\"modal-container\">\n <p>{{content}}</p>\n <button class=\"btn btn-outline-primary rounded-pill\" (click)=\"switchVisit()\">{{'Yes, switch visit' | translate }}</button>\n <button class=\"btn btn-outline-primary rounded-pill\"(click)=\"activeModal.close()\">{{'No, stay here' | translate }}</button>\n</div>\n", styles: [".modal-container{margin:10px}button{margin-right:8px}\n"], dependencies: [{ kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
7611
+ }
7612
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ModalSwitchVisitComponent, decorators: [{
7613
+ type: Component,
7614
+ args: [{ selector: 'lib-modal-switch-visit', template: "<div class=\"modal-container\">\n <p>{{content}}</p>\n <button class=\"btn btn-outline-primary rounded-pill\" (click)=\"switchVisit()\">{{'Yes, switch visit' | translate }}</button>\n <button class=\"btn btn-outline-primary rounded-pill\"(click)=\"activeModal.close()\">{{'No, stay here' | translate }}</button>\n</div>\n", styles: [".modal-container{margin:10px}button{margin-right:8px}\n"] }]
7615
+ }], ctorParameters: () => [{ type: i1$2.NgbActiveModal }, { type: i1$1.Router }, { type: ViewerService }], propDecorators: { content: [{
7616
+ type: Input
7617
+ }], model3D: [{
7618
+ type: Input
7619
+ }], spaceID: [{
7620
+ type: Input
7621
+ }], dataToMove: [{
7622
+ type: Input
7623
+ }] } });
7624
+
6019
7625
  /**
6020
- * Fallback duplication: downloads the object and re-uploads it under the new key.
6021
- *
6022
- * Storage.copy() builds the S3 `x-amz-copy-source` header by plain string concatenation
6023
- * (see @aws-amplify/storage AWSS3Provider.copy: `${bucket}/${srcPrefix}${srcKey}`), with no
6024
- * URL-encoding of the key. A key holding a space, an accent or a parenthesis therefore
6025
- * yields a header that does not match the signed canonical request, and S3 answers
6026
- * "The request signature we calculated does not match the signature you provided".
7626
+ * Parses the placement stored in a Three.js capture's metadata, or null when there is none.
6027
7627
  *
6028
- * Round-tripping the bytes avoids that header entirely and works for any key, at the cost
6029
- * of transferring the file through the browser — hence its use as a fallback only.
6030
- * IMPORTANT: like copyFileInS3, this must NOT swallow errors.
7628
+ * Both the import (writing the navigation entry) and the viewer (framing the camera) need this, so
7629
+ * it lives here rather than on either service. Placement is optional - the viewer frames the model
7630
+ * automatically without it - so unusable metadata is warned about and dropped, never thrown.
6031
7631
  */
6032
- const copyFileInS3ViaDownload = async (sourceKey, destKey) => {
6033
- const downloaded = await Storage.get(sourceKey, {
6034
- download: true,
6035
- level: "public",
6036
- });
6037
- const body = downloaded?.Body;
6038
- if (!body) {
6039
- throw new Error(`Empty download for "${sourceKey}", cannot duplicate it`);
7632
+ function parseThreeJsPlacement(metadata) {
7633
+ if (!metadata || !metadata.trim()) {
7634
+ return null;
6040
7635
  }
6041
- const result = await Storage.put(destKey, body, {
6042
- level: "public",
6043
- contentType: downloaded.ContentType || undefined,
6044
- });
6045
- return result.key;
6046
- };
6047
-
7636
+ try {
7637
+ const parsed = JSON.parse(metadata);
7638
+ const isVector = (value) => value && ["x", "y", "z"].every((axis) => typeof value[axis] === "number");
7639
+ const placement = {};
7640
+ if (isVector(parsed.cameraPosition)) {
7641
+ placement.cameraPosition = parsed.cameraPosition;
7642
+ }
7643
+ if (isVector(parsed.rotation)) {
7644
+ placement.rotation = parsed.rotation;
7645
+ }
7646
+ const source = parsed.alignment;
7647
+ if (source) {
7648
+ const alignment = {};
7649
+ if (isVector(source.translation)) {
7650
+ alignment.translation = source.translation;
7651
+ }
7652
+ if (isVector(source.rotation)) {
7653
+ alignment.rotation = source.rotation;
7654
+ }
7655
+ // Scale 0 would collapse every tag onto a point, so treat it as unset rather than honour it.
7656
+ if (typeof source.scale === "number" && source.scale > 0) {
7657
+ alignment.scale = source.scale;
7658
+ }
7659
+ if (Object.keys(alignment).length > 0) {
7660
+ placement.alignment = alignment;
7661
+ }
7662
+ }
7663
+ return placement.cameraPosition || placement.rotation || placement.alignment ? placement : null;
7664
+ }
7665
+ catch {
7666
+ console.warn("Ignoring unparseable Three.js placement metadata:", metadata);
7667
+ return null;
7668
+ }
7669
+ }
6048
7670
  function getDistanceBetweenTwoPoints(startPoint, endPoint) {
6049
7671
  const dx = endPoint.x - startPoint.x;
6050
7672
  const dy = endPoint.y - startPoint.y;
@@ -9589,11 +11211,11 @@ class SearchBarComponent {
9589
11211
  this.searchEvent.emit(results);
9590
11212
  }
9591
11213
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SearchBarComponent, deps: [{ token: SearchService }], target: i0.ɵɵFactoryTarget.Component });
9592
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SearchBarComponent, selector: "lib-search-bar", inputs: { searchable: "searchable", objectType: "objectType" }, outputs: { searchEvent: "searchEvent" }, ngImport: i0, template: "<div class=\"col-md-8 searchbar\">\r\n <div class=\"row depend\">\r\n <div class=\"col-md-5\">\r\n <input id=\"searchbar\" type=\"text\" class=\"form-control\" placeholder=\"keyword\" />\r\n </div>\r\n <div class=\"col-md-5\">\r\n <button class=\"btn btn-outline-primary rounded-pill\" (click)=\"onSubmit()\">{{ 'Search' |\r\n translate}}</button>\r\n </div>\r\n </div>\r\n</div>", styles: [".searchbar{margin-bottom:10px}\n"], dependencies: [{ kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
11214
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SearchBarComponent, selector: "lib-search-bar", inputs: { searchable: "searchable", objectType: "objectType" }, outputs: { searchEvent: "searchEvent" }, ngImport: i0, template: "<div class=\"col-md-8 searchbar\">\n <div class=\"row depend\">\n <div class=\"col-md-5\">\n <input id=\"searchbar\" type=\"text\" class=\"form-control\" placeholder=\"keyword\" />\n </div>\n <div class=\"col-md-5\">\n <button class=\"btn btn-outline-primary rounded-pill\" (click)=\"onSubmit()\">{{ 'Search' |\n translate}}</button>\n </div>\n </div>\n</div>", styles: [".searchbar{margin-bottom:10px}\n"], dependencies: [{ kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
9593
11215
  }
9594
11216
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SearchBarComponent, decorators: [{
9595
11217
  type: Component,
9596
- args: [{ selector: 'lib-search-bar', template: "<div class=\"col-md-8 searchbar\">\r\n <div class=\"row depend\">\r\n <div class=\"col-md-5\">\r\n <input id=\"searchbar\" type=\"text\" class=\"form-control\" placeholder=\"keyword\" />\r\n </div>\r\n <div class=\"col-md-5\">\r\n <button class=\"btn btn-outline-primary rounded-pill\" (click)=\"onSubmit()\">{{ 'Search' |\r\n translate}}</button>\r\n </div>\r\n </div>\r\n</div>", styles: [".searchbar{margin-bottom:10px}\n"] }]
11218
+ args: [{ selector: 'lib-search-bar', template: "<div class=\"col-md-8 searchbar\">\n <div class=\"row depend\">\n <div class=\"col-md-5\">\n <input id=\"searchbar\" type=\"text\" class=\"form-control\" placeholder=\"keyword\" />\n </div>\n <div class=\"col-md-5\">\n <button class=\"btn btn-outline-primary rounded-pill\" (click)=\"onSubmit()\">{{ 'Search' |\n translate}}</button>\n </div>\n </div>\n</div>", styles: [".searchbar{margin-bottom:10px}\n"] }]
9597
11219
  }], ctorParameters: () => [{ type: SearchService }], propDecorators: { searchable: [{
9598
11220
  type: Input
9599
11221
  }], objectType: [{
@@ -10041,11 +11663,11 @@ class LoaderComponent {
10041
11663
  document.documentElement.style.setProperty('--color', this.color);
10042
11664
  }
10043
11665
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LoaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10044
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: LoaderComponent, selector: "lib-loader", inputs: { useLogo: "useLogo", color: "color" }, ngImport: i0, template: "<div class=\"sk-fading-circle\">\r\n <img *ngIf=\"useLogo\" src=\"../../../../assets/icons/smarterplan.svg\">\r\n <div class=\"sk-circle1 sk-circle\"></div>\r\n <div class=\"sk-circle2 sk-circle\"></div>\r\n <div class=\"sk-circle3 sk-circle\"></div>\r\n <div class=\"sk-circle4 sk-circle\"></div>\r\n <div class=\"sk-circle5 sk-circle\"></div>\r\n <div class=\"sk-circle6 sk-circle\"></div>\r\n <div class=\"sk-circle7 sk-circle\"></div>\r\n <div class=\"sk-circle8 sk-circle\"></div>\r\n <div class=\"sk-circle9 sk-circle\"></div>\r\n <div class=\"sk-circle10 sk-circle\"></div>\r\n <div class=\"sk-circle11 sk-circle\"></div>\r\n <div class=\"sk-circle12 sk-circle\"></div>\r\n</div>\r\n", styles: [".sk-fading-circle{width:88px;height:88px;position:relative}.sk-fading-circle img{width:2rem;position:absolute;left:calc(50% - 16px);top:calc(50% - 16px)}.sk-fading-circle .sk-circle{width:100%;height:100%;position:absolute;left:0;top:0}.sk-fading-circle .sk-circle:before{content:\"\";display:block;margin:0 auto;width:15%;height:15%;background-color:var(--color);border-radius:100%;-webkit-animation:sk-circleFadeDelay 1.2s infinite ease-in-out both;animation:sk-circleFadeDelay 1.2s infinite ease-in-out both}.sk-fading-circle .sk-circle2{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);transform:rotate(30deg)}.sk-fading-circle .sk-circle3{-webkit-transform:rotate(60deg);-ms-transform:rotate(60deg);transform:rotate(60deg)}.sk-fading-circle .sk-circle4{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.sk-fading-circle .sk-circle5{-webkit-transform:rotate(120deg);-ms-transform:rotate(120deg);transform:rotate(120deg)}.sk-fading-circle .sk-circle6{-webkit-transform:rotate(150deg);-ms-transform:rotate(150deg);transform:rotate(150deg)}.sk-fading-circle .sk-circle7{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.sk-fading-circle .sk-circle8{-webkit-transform:rotate(210deg);-ms-transform:rotate(210deg);transform:rotate(210deg)}.sk-fading-circle .sk-circle9{-webkit-transform:rotate(240deg);-ms-transform:rotate(240deg);transform:rotate(240deg)}.sk-fading-circle .sk-circle10{-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.sk-fading-circle .sk-circle11{-webkit-transform:rotate(300deg);-ms-transform:rotate(300deg);transform:rotate(300deg)}.sk-fading-circle .sk-circle12{-webkit-transform:rotate(330deg);-ms-transform:rotate(330deg);transform:rotate(330deg)}.sk-fading-circle .sk-circle2:before{-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.sk-fading-circle .sk-circle3:before{-webkit-animation-delay:-1s;animation-delay:-1s}.sk-fading-circle .sk-circle4:before{-webkit-animation-delay:-.9s;animation-delay:-.9s}.sk-fading-circle .sk-circle5:before{-webkit-animation-delay:-.8s;animation-delay:-.8s}.sk-fading-circle .sk-circle6:before{-webkit-animation-delay:-.7s;animation-delay:-.7s}.sk-fading-circle .sk-circle7:before{-webkit-animation-delay:-.6s;animation-delay:-.6s}.sk-fading-circle .sk-circle8:before{-webkit-animation-delay:-.5s;animation-delay:-.5s}.sk-fading-circle .sk-circle9:before{-webkit-animation-delay:-.4s;animation-delay:-.4s}.sk-fading-circle .sk-circle10:before{-webkit-animation-delay:-.3s;animation-delay:-.3s}.sk-fading-circle .sk-circle11:before{-webkit-animation-delay:-.2s;animation-delay:-.2s}.sk-fading-circle .sk-circle12:before{-webkit-animation-delay:-.1s;animation-delay:-.1s}@-webkit-keyframes sk-circleFadeDelay{0%,39%,to{opacity:0}40%{opacity:1}}@keyframes sk-circleFadeDelay{0%,39%,to{opacity:0}40%{opacity:1}}\n"], dependencies: [{ kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
11666
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: LoaderComponent, selector: "lib-loader", inputs: { useLogo: "useLogo", color: "color" }, ngImport: i0, template: "<div class=\"sk-fading-circle\">\n <img *ngIf=\"useLogo\" src=\"../../../../assets/icons/smarterplan.svg\">\n <div class=\"sk-circle1 sk-circle\"></div>\n <div class=\"sk-circle2 sk-circle\"></div>\n <div class=\"sk-circle3 sk-circle\"></div>\n <div class=\"sk-circle4 sk-circle\"></div>\n <div class=\"sk-circle5 sk-circle\"></div>\n <div class=\"sk-circle6 sk-circle\"></div>\n <div class=\"sk-circle7 sk-circle\"></div>\n <div class=\"sk-circle8 sk-circle\"></div>\n <div class=\"sk-circle9 sk-circle\"></div>\n <div class=\"sk-circle10 sk-circle\"></div>\n <div class=\"sk-circle11 sk-circle\"></div>\n <div class=\"sk-circle12 sk-circle\"></div>\n</div>\n", styles: [".sk-fading-circle{width:88px;height:88px;position:relative}.sk-fading-circle img{width:2rem;position:absolute;left:calc(50% - 16px);top:calc(50% - 16px)}.sk-fading-circle .sk-circle{width:100%;height:100%;position:absolute;left:0;top:0}.sk-fading-circle .sk-circle:before{content:\"\";display:block;margin:0 auto;width:15%;height:15%;background-color:var(--color);border-radius:100%;-webkit-animation:sk-circleFadeDelay 1.2s infinite ease-in-out both;animation:sk-circleFadeDelay 1.2s infinite ease-in-out both}.sk-fading-circle .sk-circle2{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);transform:rotate(30deg)}.sk-fading-circle .sk-circle3{-webkit-transform:rotate(60deg);-ms-transform:rotate(60deg);transform:rotate(60deg)}.sk-fading-circle .sk-circle4{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.sk-fading-circle .sk-circle5{-webkit-transform:rotate(120deg);-ms-transform:rotate(120deg);transform:rotate(120deg)}.sk-fading-circle .sk-circle6{-webkit-transform:rotate(150deg);-ms-transform:rotate(150deg);transform:rotate(150deg)}.sk-fading-circle .sk-circle7{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.sk-fading-circle .sk-circle8{-webkit-transform:rotate(210deg);-ms-transform:rotate(210deg);transform:rotate(210deg)}.sk-fading-circle .sk-circle9{-webkit-transform:rotate(240deg);-ms-transform:rotate(240deg);transform:rotate(240deg)}.sk-fading-circle .sk-circle10{-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.sk-fading-circle .sk-circle11{-webkit-transform:rotate(300deg);-ms-transform:rotate(300deg);transform:rotate(300deg)}.sk-fading-circle .sk-circle12{-webkit-transform:rotate(330deg);-ms-transform:rotate(330deg);transform:rotate(330deg)}.sk-fading-circle .sk-circle2:before{-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.sk-fading-circle .sk-circle3:before{-webkit-animation-delay:-1s;animation-delay:-1s}.sk-fading-circle .sk-circle4:before{-webkit-animation-delay:-.9s;animation-delay:-.9s}.sk-fading-circle .sk-circle5:before{-webkit-animation-delay:-.8s;animation-delay:-.8s}.sk-fading-circle .sk-circle6:before{-webkit-animation-delay:-.7s;animation-delay:-.7s}.sk-fading-circle .sk-circle7:before{-webkit-animation-delay:-.6s;animation-delay:-.6s}.sk-fading-circle .sk-circle8:before{-webkit-animation-delay:-.5s;animation-delay:-.5s}.sk-fading-circle .sk-circle9:before{-webkit-animation-delay:-.4s;animation-delay:-.4s}.sk-fading-circle .sk-circle10:before{-webkit-animation-delay:-.3s;animation-delay:-.3s}.sk-fading-circle .sk-circle11:before{-webkit-animation-delay:-.2s;animation-delay:-.2s}.sk-fading-circle .sk-circle12:before{-webkit-animation-delay:-.1s;animation-delay:-.1s}@-webkit-keyframes sk-circleFadeDelay{0%,39%,to{opacity:0}40%{opacity:1}}@keyframes sk-circleFadeDelay{0%,39%,to{opacity:0}40%{opacity:1}}\n"], dependencies: [{ kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
10045
11667
  }
10046
11668
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LoaderComponent, decorators: [{
10047
11669
  type: Component,
10048
- args: [{ selector: 'lib-loader', template: "<div class=\"sk-fading-circle\">\r\n <img *ngIf=\"useLogo\" src=\"../../../../assets/icons/smarterplan.svg\">\r\n <div class=\"sk-circle1 sk-circle\"></div>\r\n <div class=\"sk-circle2 sk-circle\"></div>\r\n <div class=\"sk-circle3 sk-circle\"></div>\r\n <div class=\"sk-circle4 sk-circle\"></div>\r\n <div class=\"sk-circle5 sk-circle\"></div>\r\n <div class=\"sk-circle6 sk-circle\"></div>\r\n <div class=\"sk-circle7 sk-circle\"></div>\r\n <div class=\"sk-circle8 sk-circle\"></div>\r\n <div class=\"sk-circle9 sk-circle\"></div>\r\n <div class=\"sk-circle10 sk-circle\"></div>\r\n <div class=\"sk-circle11 sk-circle\"></div>\r\n <div class=\"sk-circle12 sk-circle\"></div>\r\n</div>\r\n", styles: [".sk-fading-circle{width:88px;height:88px;position:relative}.sk-fading-circle img{width:2rem;position:absolute;left:calc(50% - 16px);top:calc(50% - 16px)}.sk-fading-circle .sk-circle{width:100%;height:100%;position:absolute;left:0;top:0}.sk-fading-circle .sk-circle:before{content:\"\";display:block;margin:0 auto;width:15%;height:15%;background-color:var(--color);border-radius:100%;-webkit-animation:sk-circleFadeDelay 1.2s infinite ease-in-out both;animation:sk-circleFadeDelay 1.2s infinite ease-in-out both}.sk-fading-circle .sk-circle2{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);transform:rotate(30deg)}.sk-fading-circle .sk-circle3{-webkit-transform:rotate(60deg);-ms-transform:rotate(60deg);transform:rotate(60deg)}.sk-fading-circle .sk-circle4{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.sk-fading-circle .sk-circle5{-webkit-transform:rotate(120deg);-ms-transform:rotate(120deg);transform:rotate(120deg)}.sk-fading-circle .sk-circle6{-webkit-transform:rotate(150deg);-ms-transform:rotate(150deg);transform:rotate(150deg)}.sk-fading-circle .sk-circle7{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.sk-fading-circle .sk-circle8{-webkit-transform:rotate(210deg);-ms-transform:rotate(210deg);transform:rotate(210deg)}.sk-fading-circle .sk-circle9{-webkit-transform:rotate(240deg);-ms-transform:rotate(240deg);transform:rotate(240deg)}.sk-fading-circle .sk-circle10{-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.sk-fading-circle .sk-circle11{-webkit-transform:rotate(300deg);-ms-transform:rotate(300deg);transform:rotate(300deg)}.sk-fading-circle .sk-circle12{-webkit-transform:rotate(330deg);-ms-transform:rotate(330deg);transform:rotate(330deg)}.sk-fading-circle .sk-circle2:before{-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.sk-fading-circle .sk-circle3:before{-webkit-animation-delay:-1s;animation-delay:-1s}.sk-fading-circle .sk-circle4:before{-webkit-animation-delay:-.9s;animation-delay:-.9s}.sk-fading-circle .sk-circle5:before{-webkit-animation-delay:-.8s;animation-delay:-.8s}.sk-fading-circle .sk-circle6:before{-webkit-animation-delay:-.7s;animation-delay:-.7s}.sk-fading-circle .sk-circle7:before{-webkit-animation-delay:-.6s;animation-delay:-.6s}.sk-fading-circle .sk-circle8:before{-webkit-animation-delay:-.5s;animation-delay:-.5s}.sk-fading-circle .sk-circle9:before{-webkit-animation-delay:-.4s;animation-delay:-.4s}.sk-fading-circle .sk-circle10:before{-webkit-animation-delay:-.3s;animation-delay:-.3s}.sk-fading-circle .sk-circle11:before{-webkit-animation-delay:-.2s;animation-delay:-.2s}.sk-fading-circle .sk-circle12:before{-webkit-animation-delay:-.1s;animation-delay:-.1s}@-webkit-keyframes sk-circleFadeDelay{0%,39%,to{opacity:0}40%{opacity:1}}@keyframes sk-circleFadeDelay{0%,39%,to{opacity:0}40%{opacity:1}}\n"] }]
11670
+ args: [{ selector: 'lib-loader', template: "<div class=\"sk-fading-circle\">\n <img *ngIf=\"useLogo\" src=\"../../../../assets/icons/smarterplan.svg\">\n <div class=\"sk-circle1 sk-circle\"></div>\n <div class=\"sk-circle2 sk-circle\"></div>\n <div class=\"sk-circle3 sk-circle\"></div>\n <div class=\"sk-circle4 sk-circle\"></div>\n <div class=\"sk-circle5 sk-circle\"></div>\n <div class=\"sk-circle6 sk-circle\"></div>\n <div class=\"sk-circle7 sk-circle\"></div>\n <div class=\"sk-circle8 sk-circle\"></div>\n <div class=\"sk-circle9 sk-circle\"></div>\n <div class=\"sk-circle10 sk-circle\"></div>\n <div class=\"sk-circle11 sk-circle\"></div>\n <div class=\"sk-circle12 sk-circle\"></div>\n</div>\n", styles: [".sk-fading-circle{width:88px;height:88px;position:relative}.sk-fading-circle img{width:2rem;position:absolute;left:calc(50% - 16px);top:calc(50% - 16px)}.sk-fading-circle .sk-circle{width:100%;height:100%;position:absolute;left:0;top:0}.sk-fading-circle .sk-circle:before{content:\"\";display:block;margin:0 auto;width:15%;height:15%;background-color:var(--color);border-radius:100%;-webkit-animation:sk-circleFadeDelay 1.2s infinite ease-in-out both;animation:sk-circleFadeDelay 1.2s infinite ease-in-out both}.sk-fading-circle .sk-circle2{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);transform:rotate(30deg)}.sk-fading-circle .sk-circle3{-webkit-transform:rotate(60deg);-ms-transform:rotate(60deg);transform:rotate(60deg)}.sk-fading-circle .sk-circle4{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.sk-fading-circle .sk-circle5{-webkit-transform:rotate(120deg);-ms-transform:rotate(120deg);transform:rotate(120deg)}.sk-fading-circle .sk-circle6{-webkit-transform:rotate(150deg);-ms-transform:rotate(150deg);transform:rotate(150deg)}.sk-fading-circle .sk-circle7{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.sk-fading-circle .sk-circle8{-webkit-transform:rotate(210deg);-ms-transform:rotate(210deg);transform:rotate(210deg)}.sk-fading-circle .sk-circle9{-webkit-transform:rotate(240deg);-ms-transform:rotate(240deg);transform:rotate(240deg)}.sk-fading-circle .sk-circle10{-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.sk-fading-circle .sk-circle11{-webkit-transform:rotate(300deg);-ms-transform:rotate(300deg);transform:rotate(300deg)}.sk-fading-circle .sk-circle12{-webkit-transform:rotate(330deg);-ms-transform:rotate(330deg);transform:rotate(330deg)}.sk-fading-circle .sk-circle2:before{-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.sk-fading-circle .sk-circle3:before{-webkit-animation-delay:-1s;animation-delay:-1s}.sk-fading-circle .sk-circle4:before{-webkit-animation-delay:-.9s;animation-delay:-.9s}.sk-fading-circle .sk-circle5:before{-webkit-animation-delay:-.8s;animation-delay:-.8s}.sk-fading-circle .sk-circle6:before{-webkit-animation-delay:-.7s;animation-delay:-.7s}.sk-fading-circle .sk-circle7:before{-webkit-animation-delay:-.6s;animation-delay:-.6s}.sk-fading-circle .sk-circle8:before{-webkit-animation-delay:-.5s;animation-delay:-.5s}.sk-fading-circle .sk-circle9:before{-webkit-animation-delay:-.4s;animation-delay:-.4s}.sk-fading-circle .sk-circle10:before{-webkit-animation-delay:-.3s;animation-delay:-.3s}.sk-fading-circle .sk-circle11:before{-webkit-animation-delay:-.2s;animation-delay:-.2s}.sk-fading-circle .sk-circle12:before{-webkit-animation-delay:-.1s;animation-delay:-.1s}@-webkit-keyframes sk-circleFadeDelay{0%,39%,to{opacity:0}40%{opacity:1}}@keyframes sk-circleFadeDelay{0%,39%,to{opacity:0}40%{opacity:1}}\n"] }]
10049
11671
  }], ctorParameters: () => [], propDecorators: { useLogo: [{
10050
11672
  type: Input
10051
11673
  }], color: [{
@@ -10103,11 +11725,11 @@ class SupportModalComponent {
10103
11725
  }
10104
11726
  }
10105
11727
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SupportModalComponent, deps: [{ token: SupportService }, { token: i1$2.NgbActiveModal }, { token: i3.UntypedFormBuilder }, { token: BaseUserService }, { token: ValidatorsService }], target: i0.ɵɵFactoryTarget.Component });
10106
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SupportModalComponent, selector: "app-support-modal", ngImport: i0, template: "<div class=\"modal-header\">\r\n <h5 class=\"modal-title\" translate=\"Help\"></h5>\r\n</div>\r\n<div class=\"modal-body\">\r\n <div class=\"support-loader-container\">\r\n <lib-loader *ngIf=\"loading\"></lib-loader>\r\n </div>\r\n <form (ngSubmit)=\"onSubmit()\" [formGroup]=\"supportForm\" style=\"margin-top:1rem;\" *ngIf=\"!loading && status==='new'\">\r\n <div>\r\n <label>{{'Title' | translate}} *</label>\r\n <div>\r\n <input type=\"text\" class=\"form-control\" formControlName=\"title\" data-testid=\"new-support-title\"\r\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['title'])}\"\r\n maxlength=\"255\">\r\n </div>\r\n <div class=\"invalid-feedback\">\r\n <span data-testid=\"new-support-title-invalid\" [translate]=\"validatorsService.getError(supportForm.controls['title'])\"\r\n *ngIf=\" validatorsService.isControlError(supportForm.controls['title'])\"></span>\r\n </div>\r\n </div>\r\n <div *ngIf=\"!currentUser\">\r\n <label>{{'Email' | translate}} *</label>\r\n <div>\r\n <input type=\"email\" class=\"form-control\" formControlName=\"email\" data-testid=\"new-support-email\"\r\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['email'])}\"\r\n maxlength=\"255\">\r\n </div>\r\n <div class=\"invalid-feedback\">\r\n <span data-testid=\"new-support-email-invalid\" [translate]=\"validatorsService.getError(supportForm.controls['email'])\"\r\n *ngIf=\" validatorsService.isControlError(supportForm.controls['email'])\"></span>\r\n </div>\r\n </div>\r\n <div>\r\n <label>{{'Description' | translate}} *</label>\r\n <div>\r\n <textarea formControlName=\"description\" class=\"form-control\"\r\n data-testid=\"new-support-description\"\r\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['description'])}\"></textarea>\r\n </div>\r\n <div class=\"invalid-feedback\" >\r\n <span data-testid=\"new-support-description-invalid\" [translate]=\" validatorsService.getError(supportForm.controls['description'])\"\r\n *ngIf=\"validatorsService.isControlError(supportForm.controls['description'])\"></span>\r\n </div>\r\n </div>\r\n </form>\r\n\r\n <div *ngIf=\"status ==='send'\" translate=\"Your request has been registered\" data-testid=\"new-support-send-info\">\r\n </div>\r\n <div *ngIf=\"status ==='error'\" translate=\"An error has occurred, please renew your request\"\r\n data-testid=\"new-support-error-info\">\r\n\r\n </div>\r\n</div>\r\n<div class=\"modal-footer\">\r\n <button type=\"button\" class=\"btn rounded-pill btn-museum\" data-dismiss=\"modal\" translate=\"Close\" (click)=\"onClose()\"\r\n data-testid=\"new-support-close-btn\"></button>\r\n <button *ngIf=\"!loading && status==='new'\" type=\"button\" class=\"btn rounded-pill btn-museum\" translate=\"Submit\"\r\n (click)=\"onSubmit()\" [disabled]=\"!supportForm.valid\" data-testid=\"new-support-submit-btn\"></button>\r\n</div>", styles: [".support-loader-container{width:88px;margin:auto}.invalid-feedback{height:16px}\n"], dependencies: [{ kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i11.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "component", type: LoaderComponent, selector: "lib-loader", inputs: ["useLogo", "color"] }, { kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
11728
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SupportModalComponent, selector: "app-support-modal", ngImport: i0, template: "<div class=\"modal-header\">\n <h5 class=\"modal-title\" translate=\"Help\"></h5>\n</div>\n<div class=\"modal-body\">\n <div class=\"support-loader-container\">\n <lib-loader *ngIf=\"loading\"></lib-loader>\n </div>\n <form (ngSubmit)=\"onSubmit()\" [formGroup]=\"supportForm\" style=\"margin-top:1rem;\" *ngIf=\"!loading && status==='new'\">\n <div>\n <label>{{'Title' | translate}} *</label>\n <div>\n <input type=\"text\" class=\"form-control\" formControlName=\"title\" data-testid=\"new-support-title\"\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['title'])}\"\n maxlength=\"255\">\n </div>\n <div class=\"invalid-feedback\">\n <span data-testid=\"new-support-title-invalid\" [translate]=\"validatorsService.getError(supportForm.controls['title'])\"\n *ngIf=\" validatorsService.isControlError(supportForm.controls['title'])\"></span>\n </div>\n </div>\n <div *ngIf=\"!currentUser\">\n <label>{{'Email' | translate}} *</label>\n <div>\n <input type=\"email\" class=\"form-control\" formControlName=\"email\" data-testid=\"new-support-email\"\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['email'])}\"\n maxlength=\"255\">\n </div>\n <div class=\"invalid-feedback\">\n <span data-testid=\"new-support-email-invalid\" [translate]=\"validatorsService.getError(supportForm.controls['email'])\"\n *ngIf=\" validatorsService.isControlError(supportForm.controls['email'])\"></span>\n </div>\n </div>\n <div>\n <label>{{'Description' | translate}} *</label>\n <div>\n <textarea formControlName=\"description\" class=\"form-control\"\n data-testid=\"new-support-description\"\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['description'])}\"></textarea>\n </div>\n <div class=\"invalid-feedback\" >\n <span data-testid=\"new-support-description-invalid\" [translate]=\" validatorsService.getError(supportForm.controls['description'])\"\n *ngIf=\"validatorsService.isControlError(supportForm.controls['description'])\"></span>\n </div>\n </div>\n </form>\n\n <div *ngIf=\"status ==='send'\" translate=\"Your request has been registered\" data-testid=\"new-support-send-info\">\n </div>\n <div *ngIf=\"status ==='error'\" translate=\"An error has occurred, please renew your request\"\n data-testid=\"new-support-error-info\">\n\n </div>\n</div>\n<div class=\"modal-footer\">\n <button type=\"button\" class=\"btn rounded-pill btn-museum\" data-dismiss=\"modal\" translate=\"Close\" (click)=\"onClose()\"\n data-testid=\"new-support-close-btn\"></button>\n <button *ngIf=\"!loading && status==='new'\" type=\"button\" class=\"btn rounded-pill btn-museum\" translate=\"Submit\"\n (click)=\"onSubmit()\" [disabled]=\"!supportForm.valid\" data-testid=\"new-support-submit-btn\"></button>\n</div>", styles: [".support-loader-container{width:88px;margin:auto}.invalid-feedback{height:16px}\n"], dependencies: [{ kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i11.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "component", type: LoaderComponent, selector: "lib-loader", inputs: ["useLogo", "color"] }, { kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
10107
11729
  }
10108
11730
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SupportModalComponent, decorators: [{
10109
11731
  type: Component,
10110
- args: [{ selector: 'app-support-modal', template: "<div class=\"modal-header\">\r\n <h5 class=\"modal-title\" translate=\"Help\"></h5>\r\n</div>\r\n<div class=\"modal-body\">\r\n <div class=\"support-loader-container\">\r\n <lib-loader *ngIf=\"loading\"></lib-loader>\r\n </div>\r\n <form (ngSubmit)=\"onSubmit()\" [formGroup]=\"supportForm\" style=\"margin-top:1rem;\" *ngIf=\"!loading && status==='new'\">\r\n <div>\r\n <label>{{'Title' | translate}} *</label>\r\n <div>\r\n <input type=\"text\" class=\"form-control\" formControlName=\"title\" data-testid=\"new-support-title\"\r\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['title'])}\"\r\n maxlength=\"255\">\r\n </div>\r\n <div class=\"invalid-feedback\">\r\n <span data-testid=\"new-support-title-invalid\" [translate]=\"validatorsService.getError(supportForm.controls['title'])\"\r\n *ngIf=\" validatorsService.isControlError(supportForm.controls['title'])\"></span>\r\n </div>\r\n </div>\r\n <div *ngIf=\"!currentUser\">\r\n <label>{{'Email' | translate}} *</label>\r\n <div>\r\n <input type=\"email\" class=\"form-control\" formControlName=\"email\" data-testid=\"new-support-email\"\r\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['email'])}\"\r\n maxlength=\"255\">\r\n </div>\r\n <div class=\"invalid-feedback\">\r\n <span data-testid=\"new-support-email-invalid\" [translate]=\"validatorsService.getError(supportForm.controls['email'])\"\r\n *ngIf=\" validatorsService.isControlError(supportForm.controls['email'])\"></span>\r\n </div>\r\n </div>\r\n <div>\r\n <label>{{'Description' | translate}} *</label>\r\n <div>\r\n <textarea formControlName=\"description\" class=\"form-control\"\r\n data-testid=\"new-support-description\"\r\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['description'])}\"></textarea>\r\n </div>\r\n <div class=\"invalid-feedback\" >\r\n <span data-testid=\"new-support-description-invalid\" [translate]=\" validatorsService.getError(supportForm.controls['description'])\"\r\n *ngIf=\"validatorsService.isControlError(supportForm.controls['description'])\"></span>\r\n </div>\r\n </div>\r\n </form>\r\n\r\n <div *ngIf=\"status ==='send'\" translate=\"Your request has been registered\" data-testid=\"new-support-send-info\">\r\n </div>\r\n <div *ngIf=\"status ==='error'\" translate=\"An error has occurred, please renew your request\"\r\n data-testid=\"new-support-error-info\">\r\n\r\n </div>\r\n</div>\r\n<div class=\"modal-footer\">\r\n <button type=\"button\" class=\"btn rounded-pill btn-museum\" data-dismiss=\"modal\" translate=\"Close\" (click)=\"onClose()\"\r\n data-testid=\"new-support-close-btn\"></button>\r\n <button *ngIf=\"!loading && status==='new'\" type=\"button\" class=\"btn rounded-pill btn-museum\" translate=\"Submit\"\r\n (click)=\"onSubmit()\" [disabled]=\"!supportForm.valid\" data-testid=\"new-support-submit-btn\"></button>\r\n</div>", styles: [".support-loader-container{width:88px;margin:auto}.invalid-feedback{height:16px}\n"] }]
11732
+ args: [{ selector: 'app-support-modal', template: "<div class=\"modal-header\">\n <h5 class=\"modal-title\" translate=\"Help\"></h5>\n</div>\n<div class=\"modal-body\">\n <div class=\"support-loader-container\">\n <lib-loader *ngIf=\"loading\"></lib-loader>\n </div>\n <form (ngSubmit)=\"onSubmit()\" [formGroup]=\"supportForm\" style=\"margin-top:1rem;\" *ngIf=\"!loading && status==='new'\">\n <div>\n <label>{{'Title' | translate}} *</label>\n <div>\n <input type=\"text\" class=\"form-control\" formControlName=\"title\" data-testid=\"new-support-title\"\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['title'])}\"\n maxlength=\"255\">\n </div>\n <div class=\"invalid-feedback\">\n <span data-testid=\"new-support-title-invalid\" [translate]=\"validatorsService.getError(supportForm.controls['title'])\"\n *ngIf=\" validatorsService.isControlError(supportForm.controls['title'])\"></span>\n </div>\n </div>\n <div *ngIf=\"!currentUser\">\n <label>{{'Email' | translate}} *</label>\n <div>\n <input type=\"email\" class=\"form-control\" formControlName=\"email\" data-testid=\"new-support-email\"\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['email'])}\"\n maxlength=\"255\">\n </div>\n <div class=\"invalid-feedback\">\n <span data-testid=\"new-support-email-invalid\" [translate]=\"validatorsService.getError(supportForm.controls['email'])\"\n *ngIf=\" validatorsService.isControlError(supportForm.controls['email'])\"></span>\n </div>\n </div>\n <div>\n <label>{{'Description' | translate}} *</label>\n <div>\n <textarea formControlName=\"description\" class=\"form-control\"\n data-testid=\"new-support-description\"\n [ngClass]=\"{'is-invalid' : validatorsService.isControlError(supportForm.controls['description'])}\"></textarea>\n </div>\n <div class=\"invalid-feedback\" >\n <span data-testid=\"new-support-description-invalid\" [translate]=\" validatorsService.getError(supportForm.controls['description'])\"\n *ngIf=\"validatorsService.isControlError(supportForm.controls['description'])\"></span>\n </div>\n </div>\n </form>\n\n <div *ngIf=\"status ==='send'\" translate=\"Your request has been registered\" data-testid=\"new-support-send-info\">\n </div>\n <div *ngIf=\"status ==='error'\" translate=\"An error has occurred, please renew your request\"\n data-testid=\"new-support-error-info\">\n\n </div>\n</div>\n<div class=\"modal-footer\">\n <button type=\"button\" class=\"btn rounded-pill btn-museum\" data-dismiss=\"modal\" translate=\"Close\" (click)=\"onClose()\"\n data-testid=\"new-support-close-btn\"></button>\n <button *ngIf=\"!loading && status==='new'\" type=\"button\" class=\"btn rounded-pill btn-museum\" translate=\"Submit\"\n (click)=\"onSubmit()\" [disabled]=\"!supportForm.valid\" data-testid=\"new-support-submit-btn\"></button>\n</div>", styles: [".support-loader-container{width:88px;margin:auto}.invalid-feedback{height:16px}\n"] }]
10111
11733
  }], ctorParameters: () => [{ type: SupportService }, { type: i1$2.NgbActiveModal }, { type: i3.UntypedFormBuilder }, { type: BaseUserService }, { type: ValidatorsService }] });
10112
11734
 
10113
11735
  class LocaleService {
@@ -11523,7 +13145,15 @@ class VisitService {
11523
13145
  this.planService = planService;
11524
13146
  this.userService = userService;
11525
13147
  }
11526
- async getVisitsForSpaceForUser(spaceID) {
13148
+ /**
13149
+ * The visits a user may open for a space.
13150
+ *
13151
+ * `includeImageCount` drives one S3 listing per node, awaited in sequence, purely to fill
13152
+ * `nmbImagesImported`. Callers that only need to know which visits exist - the viewer's model
13153
+ * picker, for one - should pass false: waiting on S3 to draw a button is a visible delay, and
13154
+ * the count is not read on that path.
13155
+ */
13156
+ async getVisitsForSpaceForUser(spaceID, includeImageCount = true) {
11527
13157
  const visits = [];
11528
13158
  const missionsForSpace = this.userService.currentMissions.filter((mis) => mis.spaceID === spaceID);
11529
13159
  const allAllowed = this.userService.userHasAccessToAllZonesInSpace(spaceID);
@@ -11541,9 +13171,12 @@ class VisitService {
11541
13171
  }
11542
13172
  for (const node of allAllowedNodes) {
11543
13173
  // console.log(node)
11544
- const folder = `visits/${node.model3D}/sweeps/`;
11545
- const files = await listFilesInFolder(folder);
11546
- const nmbImages = files.length;
13174
+ let nmbImages = 0;
13175
+ if (includeImageCount) {
13176
+ const folder = `visits/${node.model3D}/sweeps/`;
13177
+ const files = await listFilesInFolder(folder);
13178
+ nmbImages = files.length;
13179
+ }
11547
13180
  const capture = captures.find((cap) => cap.nodes.items.includes(node));
11548
13181
  const visit = {
11549
13182
  captureID: node.captureID,
@@ -11555,6 +13188,7 @@ class VisitService {
11555
13188
  node,
11556
13189
  name: node.metadata,
11557
13190
  nmbImagesImported: nmbImages,
13191
+ captureMetadata: capture.metadata,
11558
13192
  };
11559
13193
  visits.push(visit);
11560
13194
  }
@@ -12343,12 +13977,31 @@ class SpaceService {
12343
13977
  }
12344
13978
  return null;
12345
13979
  }
13980
+ /**
13981
+ * The visit the current user is allowed to open for this space.
13982
+ *
13983
+ * Callers need more than the model reference - which viewer the visit was captured with decides
13984
+ * whether it opens in Matterport or Three.js - so the whole visit is returned here.
13985
+ */
13986
+ async getAllowedVisitForSpace(spaceID) {
13987
+ const visits = await this.getAllowedVisitsForSpace(spaceID);
13988
+ return visits.length > 0 ? visits[0] : null;
13989
+ }
13990
+ /**
13991
+ * Every visit the current user may open for this space.
13992
+ *
13993
+ * A site can carry more than one - a Matterport capture plus one or more 3D models - and the
13994
+ * viewer offers them as a list, so the caller needs all of them rather than just the first.
13995
+ */
13996
+ async getAllowedVisitsForSpace(spaceID) {
13997
+ // No image count: this feeds the viewer, which only needs to know which visits exist. Asking
13998
+ // for it would add an S3 listing per node before anything can be drawn.
13999
+ const visits = await this.visitService.getVisitsForSpaceForUser(spaceID, false);
14000
+ return visits ?? [];
14001
+ }
12346
14002
  async getAllowedModel3DForSpace(spaceID) {
12347
- const visits = await this.visitService.getVisitsForSpaceForUser(spaceID);
12348
- if (visits.length > 0) {
12349
- return visits[0].model3d;
12350
- }
12351
- return null;
14003
+ const visit = await this.getAllowedVisitForSpace(spaceID);
14004
+ return visit ? visit.model3d : null;
12352
14005
  }
12353
14006
  async softDeleteSpace(spaceID) {
12354
14007
  await this.updateSpace({
@@ -12812,11 +14465,11 @@ class NavigationBarComponent {
12812
14465
  this.baseVisibilityService.isChangePositionVisible.next(false);
12813
14466
  }
12814
14467
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NavigationBarComponent, deps: [{ token: 'currentAwsKinesisAnalyticsService' }, { token: i1$1.Router }, { token: SpaceService }, { token: BaseUserService }, { token: ZoneService }, { token: VisitService }, { token: ViewerService }, { token: NavigatorService }, { token: ZoneChangeService }, { token: BaseVisibilityService }], target: i0.ɵɵFactoryTarget.Component });
12815
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: NavigationBarComponent, selector: "lib-navigation-bar", viewQueries: [{ propertyName: "navbarSelectMenu", first: true, predicate: ["navbarSelectMenu"], descendants: true }], ngImport: i0, template: "<nav class=\"navbar navbar-expand-md navbar-dark navbar-inverse second-nav\">\r\n <div class=\"d-flex w-100\">\r\n <div #navbarSelectMenu class=\"w-100\" id=\"nav-item-container-blue\">\r\n <div class=\"d-flex flex-column flex-md-row p-1 mt-2\">\r\n <select class=\"form-control flex-fill flex-lg-fill-unset location select-menu\"\r\n [(ngModel)]=\"selectedLocation\" (ngModelChange)=\"onLocationClick()\">\r\n <option [ngValue]=\"null\" selected>{{ 'Location' | translate }}</option>\r\n <option *ngFor=\"let loc of locations; index as index\" [ngValue]=\"loc\"> {{ loc.name }}</option>\r\n </select>\r\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu\" (change)=\"onVisitClick($event)\"\r\n *ngIf=\"visitsPerSpace && visitsPerSpace.length > 1\">\r\n <option value=\"default\" selected disabled hidden>{{'Visit' | translate}}</option>\r\n <option *ngFor=\"let visit of visitsPerSpace; index as index\" value=\"{{index}}\">{{ visit.name}}\r\n </option>\r\n </select>\r\n <select class=\"form-control flex-fill flex-lg-fill-unset floor select-menu\" [(ngModel)]=\"selectedFloor\"\r\n (ngModelChange)=\"onFloorClick()\">\r\n <option [ngValue]=\"null\" selected>{{'Floor' | translate}}</option>\r\n <option *ngFor=\"let floor of floorsPerSpace\" [ngValue]=\"floor\">{{ floor.name}}</option>\r\n </select>\r\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu zone\" [(ngModel)]=\"selectedZone\"\r\n (ngModelChange)=\"onZoneClick()\">\r\n <option [ngValue]=\"null\" selected>{{'Zone' | translate}}</option>\r\n <option *ngFor=\"let zone of zonesPerFloor\" [ngValue]=\"zone\"><span\r\n *ngIf=\"zone.code_ref\">{{zone.code_ref}}_</span>{{zone.name}}</option>\r\n </select>\r\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu\" [(ngModel)]=\"selectedChildZone\"\r\n *ngIf=\"childrenZones && childrenZones.length>0\" (ngModelChange)=\"onChildZoneClick()\">\r\n <option [ngValue]=\"null\" selected disabled>{{'Zone' | translate}}</option>\r\n <option *ngFor=\"let zone of childrenZones\" [ngValue]=\"zone\">{{ zone.name}}</option>\r\n </select>\r\n <button (click)=\"goTo()\"\r\n class=\"btn btn-outline-primary rounded-pill mx-2 w-md-fit-content h-md-fit-content align-self-md-center btn-3d d-none d-lg-block\"\r\n [disabled]=\"!selectedLocation || !selectedModel3d || !canGo()\">\r\n <i class=\"icon-go-to-3d d-inline-block\"></i> {{'3D visit' | translate}}\r\n </button>\r\n </div>\r\n <div class=\"row align-items-center justify-content-center ml-0 p-1\" *ngIf=\"showLocation()\">\r\n <p class=\"fw-bold lh-base align-middle text-center\">\r\n {{ showLocation() }}\r\n </p>\r\n </div>\r\n <div class=\"row align-items-center justify-content-center ml-0 p-1 d-block d-lg-none\">\r\n <button (click)=\"goTo()\"\r\n class=\"btn btn-outline-primary rounded-pill w-25 mx-2 w-md-fit-content h-md-fit-content align-self-md-center btn-3d\"\r\n [disabled]=\"!selectedLocation || !selectedModel3d || !canGo()\">\r\n <i class=\"icon-go-to-3d d-inline-block\"></i> {{'3D visit' | translate}}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n</nav>", styles: [":host{position:sticky;top:0;height:123px}.navbar{z-index:200;padding:.4rem 1rem}.navbar.second-nav{z-index:199;background-color:var(--smarterplan-primary)}input,select{margin-right:15px}.select{background:transparent;border:none;color:#fff;margin:6px 20px 0}@media (min-width: 992px){.flex-lg-fill-unset{flex:unset!important}}.form-control{width:100%;min-width:80px;max-width:18%}.location{width:auto;max-width:35%}.floor,.zone{width:auto;max-width:30%}button{color:#fff;padding:5px 10px;font-size:1.1em}.navbar-toggler{margin-right:1em}.select-menu{max-width:inherit}.btn-3d .icon-go-to-3d{width:30px;height:30px;background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"utf-8\"?>%0D%0A<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->%0D%0A<svg version=\"1.1\" id=\"Calque_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"%0D%0A%09 viewBox=\"0 0 60 60\" style=\"enable-background:new 0 0 60 60;\" xml:space=\"preserve\">%0D%0A<style type=\"text/css\">%0D%0A%09.st0{fill:%23565656;}%0D%0A<\\/style>%0D%0A<g>%0D%0A%09<path class=\"st0\" d=\"M43.9,32.1V17.7c0-0.4-0.2-0.7-0.5-0.9l-12.8-8c-0.2-0.1-0.4-0.2-0.6-0.2c-0.2,0-0.4,0.1-0.6,0.2l-12.8,8%0D%0A%09%09c-0.3,0.2-0.5,0.5-0.5,0.9v14.4c0,0.4,0.2,0.7,0.5,0.9l12.8,8c0.3,0.2,0.8,0.2,1.1,0l12.8-8C43.7,32.8,43.9,32.5,43.9,32.1z%0D%0A%09%09 M28.9,38.2l-10.7-6.7v-12l10.7,6.7V38.2z M30,24.4l-10.8-6.8L30,10.9l10.8,6.8L30,24.4z M41.8,31.5l-10.7,6.7v-12l10.7-6.7V31.5z\"%0D%0A%09%09/>%0D%0A%09<path class=\"st0\" d=\"M56.1,29.3l-10.3-6.8v2l1.4,1v0l0.8,0.5l0,0l7,4.8c0.3,0.2,0.4,0.5,0.4,0.8c0,0.3-0.2,0.6-0.4,0.8l-6.8,4.7%0D%0A%09%09l0,0l-0.9,0.7v0l-2.9,2v0l-13.8,9.6c-0.3,0.2-0.8,0.2-1.1,0l-14.2-9.8l-2.6-1.8v0L5,32.4c-0.3-0.2-0.4-0.5-0.4-0.8%0D%0A%09%09c0-0.3,0.2-0.6,0.4-0.8l0,0l7.3-5l0,0l0.5-0.3v0l1.4-1v-2L3.9,29.3c-0.8,0.5-1.2,1.4-1.2,2.4c0,0.9,0.5,1.8,1.2,2.4l8.8,6.1v0%0D%0A%09%09l4.5,3.1l11.2,7.7c1,0.7,2.3,0.7,3.3,0l13.7-9.5l1.9-1.3v0l8.8-6.1c0.8-0.5,1.2-1.4,1.2-2.4S56.9,29.8,56.1,29.3z\"/>%0D%0A</g>%0D%0A</svg>%0D%0A');border:none;vertical-align:middle;fill:var(--smarterplan-primary)}.btn-3d:hover:enabled .icon-go-to-3d{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"utf-8\"?>%0D%0A<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->%0D%0A<svg version=\"1.1\" id=\"Calque_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"%0D%0A%09 viewBox=\"0 0 60 60\" style=\"enable-background:new 0 0 60 60;\" xml:space=\"preserve\">%0D%0A<style type=\"text/css\">%0D%0A%09.st0{fill:%23FFFFFF;}%0D%0A<\\/style>%0D%0A<g>%0D%0A%09<path class=\"st0\" d=\"M43.9,32.1V17.7c0-0.4-0.2-0.7-0.5-0.9l-12.8-8c-0.2-0.1-0.4-0.2-0.6-0.2c-0.2,0-0.4,0.1-0.6,0.2l-12.8,8%0D%0A%09%09c-0.3,0.2-0.5,0.5-0.5,0.9v14.4c0,0.4,0.2,0.7,0.5,0.9l12.8,8c0.3,0.2,0.8,0.2,1.1,0l12.8-8C43.7,32.8,43.9,32.5,43.9,32.1z%0D%0A%09%09 M28.9,38.2l-10.7-6.7v-12l10.7,6.7V38.2z M30,24.4l-10.8-6.8L30,10.9l10.8,6.8L30,24.4z M41.8,31.5l-10.7,6.7v-12l10.7-6.7V31.5z\"%0D%0A%09%09/>%0D%0A%09<path class=\"st0\" d=\"M56.1,29.3l-10.3-6.8v2l1.4,1v0l0.8,0.5l0,0l7,4.8c0.3,0.2,0.4,0.5,0.4,0.8c0,0.3-0.2,0.6-0.4,0.8l-6.8,4.7%0D%0A%09%09l0,0l-0.9,0.7v0l-2.9,2v0l-13.8,9.6c-0.3,0.2-0.8,0.2-1.1,0l-14.2-9.8l-2.6-1.8v0L5,32.4c-0.3-0.2-0.4-0.5-0.4-0.8%0D%0A%09%09c0-0.3,0.2-0.6,0.4-0.8l0,0l7.3-5l0,0l0.5-0.3v0l1.4-1v-2L3.9,29.3c-0.8,0.5-1.2,1.4-1.2,2.4c0,0.9,0.5,1.8,1.2,2.4l8.8,6.1v0%0D%0A%09%09l4.5,3.1l11.2,7.7c1,0.7,2.3,0.7,3.3,0l13.7-9.5l1.9-1.3v0l8.8-6.1c0.8-0.5,1.2-1.4,1.2-2.4S56.9,29.8,56.1,29.3z\"/>%0D%0A</g>%0D%0A</svg>%0D%0A')}.btn-3d[disabled]:hover .icon-go-to-3d{fill:var(--smarterplan-primary)}.outer_circle{background-color:#ebebeb;border-radius:50%;width:32px;height:32px;padding:1px}@media (max-width: 767px){.navbar{padding:.2rem .5rem}.form-control,.location,.floor,.zone{max-width:100%;margin-bottom:15px}input,select{margin-right:0;margin-bottom:15px}}\n"], dependencies: [{ kind: "directive", type: i3.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i11.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
14468
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: NavigationBarComponent, selector: "lib-navigation-bar", viewQueries: [{ propertyName: "navbarSelectMenu", first: true, predicate: ["navbarSelectMenu"], descendants: true }], ngImport: i0, template: "<nav class=\"navbar navbar-expand-md navbar-dark navbar-inverse second-nav\">\n <div class=\"d-flex w-100\">\n <div #navbarSelectMenu class=\"w-100\" id=\"nav-item-container-blue\">\n <div class=\"d-flex flex-column flex-md-row p-1 mt-2\">\n <select class=\"form-control flex-fill flex-lg-fill-unset location select-menu\"\n [(ngModel)]=\"selectedLocation\" (ngModelChange)=\"onLocationClick()\">\n <option [ngValue]=\"null\" selected>{{ 'Location' | translate }}</option>\n <option *ngFor=\"let loc of locations; index as index\" [ngValue]=\"loc\"> {{ loc.name }}</option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu\" (change)=\"onVisitClick($event)\"\n *ngIf=\"visitsPerSpace && visitsPerSpace.length > 1\">\n <option value=\"default\" selected disabled hidden>{{'Visit' | translate}}</option>\n <option *ngFor=\"let visit of visitsPerSpace; index as index\" value=\"{{index}}\">{{ visit.name}}\n </option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset floor select-menu\" [(ngModel)]=\"selectedFloor\"\n (ngModelChange)=\"onFloorClick()\">\n <option [ngValue]=\"null\" selected>{{'Floor' | translate}}</option>\n <option *ngFor=\"let floor of floorsPerSpace\" [ngValue]=\"floor\">{{ floor.name}}</option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu zone\" [(ngModel)]=\"selectedZone\"\n (ngModelChange)=\"onZoneClick()\">\n <option [ngValue]=\"null\" selected>{{'Zone' | translate}}</option>\n <option *ngFor=\"let zone of zonesPerFloor\" [ngValue]=\"zone\"><span\n *ngIf=\"zone.code_ref\">{{zone.code_ref}}_</span>{{zone.name}}</option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu\" [(ngModel)]=\"selectedChildZone\"\n *ngIf=\"childrenZones && childrenZones.length>0\" (ngModelChange)=\"onChildZoneClick()\">\n <option [ngValue]=\"null\" selected disabled>{{'Zone' | translate}}</option>\n <option *ngFor=\"let zone of childrenZones\" [ngValue]=\"zone\">{{ zone.name}}</option>\n </select>\n <button (click)=\"goTo()\"\n class=\"btn btn-outline-primary rounded-pill mx-2 w-md-fit-content h-md-fit-content align-self-md-center btn-3d d-none d-lg-block\"\n [disabled]=\"!selectedLocation || !selectedModel3d || !canGo()\">\n <i class=\"icon-go-to-3d d-inline-block\"></i> {{'3D visit' | translate}}\n </button>\n </div>\n <div class=\"row align-items-center justify-content-center ml-0 p-1\" *ngIf=\"showLocation()\">\n <p class=\"fw-bold lh-base align-middle text-center\">\n {{ showLocation() }}\n </p>\n </div>\n <div class=\"row align-items-center justify-content-center ml-0 p-1 d-block d-lg-none\">\n <button (click)=\"goTo()\"\n class=\"btn btn-outline-primary rounded-pill w-25 mx-2 w-md-fit-content h-md-fit-content align-self-md-center btn-3d\"\n [disabled]=\"!selectedLocation || !selectedModel3d || !canGo()\">\n <i class=\"icon-go-to-3d d-inline-block\"></i> {{'3D visit' | translate}}\n </button>\n </div>\n </div>\n </div>\n</nav>", styles: [":host{position:sticky;top:0;height:123px}.navbar{z-index:200;padding:.4rem 1rem}.navbar.second-nav{z-index:199;background-color:var(--smarterplan-primary)}input,select{margin-right:15px}.select{background:transparent;border:none;color:#fff;margin:6px 20px 0}@media (min-width: 992px){.flex-lg-fill-unset{flex:unset!important}}.form-control{width:100%;min-width:80px;max-width:18%}.location{width:auto;max-width:35%}.floor,.zone{width:auto;max-width:30%}button{color:#fff;padding:5px 10px;font-size:1.1em}.navbar-toggler{margin-right:1em}.select-menu{max-width:inherit}.btn-3d .icon-go-to-3d{width:30px;height:30px;background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"utf-8\"?>%0A<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->%0A<svg version=\"1.1\" id=\"Calque_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"%0A%09 viewBox=\"0 0 60 60\" style=\"enable-background:new 0 0 60 60;\" xml:space=\"preserve\">%0A<style type=\"text/css\">%0A%09.st0{fill:%23565656;}%0A<\\/style>%0A<g>%0A%09<path class=\"st0\" d=\"M43.9,32.1V17.7c0-0.4-0.2-0.7-0.5-0.9l-12.8-8c-0.2-0.1-0.4-0.2-0.6-0.2c-0.2,0-0.4,0.1-0.6,0.2l-12.8,8%0A%09%09c-0.3,0.2-0.5,0.5-0.5,0.9v14.4c0,0.4,0.2,0.7,0.5,0.9l12.8,8c0.3,0.2,0.8,0.2,1.1,0l12.8-8C43.7,32.8,43.9,32.5,43.9,32.1z%0A%09%09 M28.9,38.2l-10.7-6.7v-12l10.7,6.7V38.2z M30,24.4l-10.8-6.8L30,10.9l10.8,6.8L30,24.4z M41.8,31.5l-10.7,6.7v-12l10.7-6.7V31.5z\"%0A%09%09/>%0A%09<path class=\"st0\" d=\"M56.1,29.3l-10.3-6.8v2l1.4,1v0l0.8,0.5l0,0l7,4.8c0.3,0.2,0.4,0.5,0.4,0.8c0,0.3-0.2,0.6-0.4,0.8l-6.8,4.7%0A%09%09l0,0l-0.9,0.7v0l-2.9,2v0l-13.8,9.6c-0.3,0.2-0.8,0.2-1.1,0l-14.2-9.8l-2.6-1.8v0L5,32.4c-0.3-0.2-0.4-0.5-0.4-0.8%0A%09%09c0-0.3,0.2-0.6,0.4-0.8l0,0l7.3-5l0,0l0.5-0.3v0l1.4-1v-2L3.9,29.3c-0.8,0.5-1.2,1.4-1.2,2.4c0,0.9,0.5,1.8,1.2,2.4l8.8,6.1v0%0A%09%09l4.5,3.1l11.2,7.7c1,0.7,2.3,0.7,3.3,0l13.7-9.5l1.9-1.3v0l8.8-6.1c0.8-0.5,1.2-1.4,1.2-2.4S56.9,29.8,56.1,29.3z\"/>%0A</g>%0A</svg>%0A');border:none;vertical-align:middle;fill:var(--smarterplan-primary)}.btn-3d:hover:enabled .icon-go-to-3d{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"utf-8\"?>%0A<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->%0A<svg version=\"1.1\" id=\"Calque_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"%0A%09 viewBox=\"0 0 60 60\" style=\"enable-background:new 0 0 60 60;\" xml:space=\"preserve\">%0A<style type=\"text/css\">%0A%09.st0{fill:%23FFFFFF;}%0A<\\/style>%0A<g>%0A%09<path class=\"st0\" d=\"M43.9,32.1V17.7c0-0.4-0.2-0.7-0.5-0.9l-12.8-8c-0.2-0.1-0.4-0.2-0.6-0.2c-0.2,0-0.4,0.1-0.6,0.2l-12.8,8%0A%09%09c-0.3,0.2-0.5,0.5-0.5,0.9v14.4c0,0.4,0.2,0.7,0.5,0.9l12.8,8c0.3,0.2,0.8,0.2,1.1,0l12.8-8C43.7,32.8,43.9,32.5,43.9,32.1z%0A%09%09 M28.9,38.2l-10.7-6.7v-12l10.7,6.7V38.2z M30,24.4l-10.8-6.8L30,10.9l10.8,6.8L30,24.4z M41.8,31.5l-10.7,6.7v-12l10.7-6.7V31.5z\"%0A%09%09/>%0A%09<path class=\"st0\" d=\"M56.1,29.3l-10.3-6.8v2l1.4,1v0l0.8,0.5l0,0l7,4.8c0.3,0.2,0.4,0.5,0.4,0.8c0,0.3-0.2,0.6-0.4,0.8l-6.8,4.7%0A%09%09l0,0l-0.9,0.7v0l-2.9,2v0l-13.8,9.6c-0.3,0.2-0.8,0.2-1.1,0l-14.2-9.8l-2.6-1.8v0L5,32.4c-0.3-0.2-0.4-0.5-0.4-0.8%0A%09%09c0-0.3,0.2-0.6,0.4-0.8l0,0l7.3-5l0,0l0.5-0.3v0l1.4-1v-2L3.9,29.3c-0.8,0.5-1.2,1.4-1.2,2.4c0,0.9,0.5,1.8,1.2,2.4l8.8,6.1v0%0A%09%09l4.5,3.1l11.2,7.7c1,0.7,2.3,0.7,3.3,0l13.7-9.5l1.9-1.3v0l8.8-6.1c0.8-0.5,1.2-1.4,1.2-2.4S56.9,29.8,56.1,29.3z\"/>%0A</g>%0A</svg>%0A')}.btn-3d[disabled]:hover .icon-go-to-3d{fill:var(--smarterplan-primary)}.outer_circle{background-color:#ebebeb;border-radius:50%;width:32px;height:32px;padding:1px}@media (max-width: 767px){.navbar{padding:.2rem .5rem}.form-control,.location,.floor,.zone{max-width:100%;margin-bottom:15px}input,select{margin-right:0;margin-bottom:15px}}\n"], dependencies: [{ kind: "directive", type: i3.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i11.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1.TranslatePipe, name: "translate" }] });
12816
14469
  }
12817
14470
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NavigationBarComponent, decorators: [{
12818
14471
  type: Component,
12819
- args: [{ selector: 'lib-navigation-bar', template: "<nav class=\"navbar navbar-expand-md navbar-dark navbar-inverse second-nav\">\r\n <div class=\"d-flex w-100\">\r\n <div #navbarSelectMenu class=\"w-100\" id=\"nav-item-container-blue\">\r\n <div class=\"d-flex flex-column flex-md-row p-1 mt-2\">\r\n <select class=\"form-control flex-fill flex-lg-fill-unset location select-menu\"\r\n [(ngModel)]=\"selectedLocation\" (ngModelChange)=\"onLocationClick()\">\r\n <option [ngValue]=\"null\" selected>{{ 'Location' | translate }}</option>\r\n <option *ngFor=\"let loc of locations; index as index\" [ngValue]=\"loc\"> {{ loc.name }}</option>\r\n </select>\r\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu\" (change)=\"onVisitClick($event)\"\r\n *ngIf=\"visitsPerSpace && visitsPerSpace.length > 1\">\r\n <option value=\"default\" selected disabled hidden>{{'Visit' | translate}}</option>\r\n <option *ngFor=\"let visit of visitsPerSpace; index as index\" value=\"{{index}}\">{{ visit.name}}\r\n </option>\r\n </select>\r\n <select class=\"form-control flex-fill flex-lg-fill-unset floor select-menu\" [(ngModel)]=\"selectedFloor\"\r\n (ngModelChange)=\"onFloorClick()\">\r\n <option [ngValue]=\"null\" selected>{{'Floor' | translate}}</option>\r\n <option *ngFor=\"let floor of floorsPerSpace\" [ngValue]=\"floor\">{{ floor.name}}</option>\r\n </select>\r\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu zone\" [(ngModel)]=\"selectedZone\"\r\n (ngModelChange)=\"onZoneClick()\">\r\n <option [ngValue]=\"null\" selected>{{'Zone' | translate}}</option>\r\n <option *ngFor=\"let zone of zonesPerFloor\" [ngValue]=\"zone\"><span\r\n *ngIf=\"zone.code_ref\">{{zone.code_ref}}_</span>{{zone.name}}</option>\r\n </select>\r\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu\" [(ngModel)]=\"selectedChildZone\"\r\n *ngIf=\"childrenZones && childrenZones.length>0\" (ngModelChange)=\"onChildZoneClick()\">\r\n <option [ngValue]=\"null\" selected disabled>{{'Zone' | translate}}</option>\r\n <option *ngFor=\"let zone of childrenZones\" [ngValue]=\"zone\">{{ zone.name}}</option>\r\n </select>\r\n <button (click)=\"goTo()\"\r\n class=\"btn btn-outline-primary rounded-pill mx-2 w-md-fit-content h-md-fit-content align-self-md-center btn-3d d-none d-lg-block\"\r\n [disabled]=\"!selectedLocation || !selectedModel3d || !canGo()\">\r\n <i class=\"icon-go-to-3d d-inline-block\"></i> {{'3D visit' | translate}}\r\n </button>\r\n </div>\r\n <div class=\"row align-items-center justify-content-center ml-0 p-1\" *ngIf=\"showLocation()\">\r\n <p class=\"fw-bold lh-base align-middle text-center\">\r\n {{ showLocation() }}\r\n </p>\r\n </div>\r\n <div class=\"row align-items-center justify-content-center ml-0 p-1 d-block d-lg-none\">\r\n <button (click)=\"goTo()\"\r\n class=\"btn btn-outline-primary rounded-pill w-25 mx-2 w-md-fit-content h-md-fit-content align-self-md-center btn-3d\"\r\n [disabled]=\"!selectedLocation || !selectedModel3d || !canGo()\">\r\n <i class=\"icon-go-to-3d d-inline-block\"></i> {{'3D visit' | translate}}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n</nav>", styles: [":host{position:sticky;top:0;height:123px}.navbar{z-index:200;padding:.4rem 1rem}.navbar.second-nav{z-index:199;background-color:var(--smarterplan-primary)}input,select{margin-right:15px}.select{background:transparent;border:none;color:#fff;margin:6px 20px 0}@media (min-width: 992px){.flex-lg-fill-unset{flex:unset!important}}.form-control{width:100%;min-width:80px;max-width:18%}.location{width:auto;max-width:35%}.floor,.zone{width:auto;max-width:30%}button{color:#fff;padding:5px 10px;font-size:1.1em}.navbar-toggler{margin-right:1em}.select-menu{max-width:inherit}.btn-3d .icon-go-to-3d{width:30px;height:30px;background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"utf-8\"?>%0D%0A<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->%0D%0A<svg version=\"1.1\" id=\"Calque_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"%0D%0A%09 viewBox=\"0 0 60 60\" style=\"enable-background:new 0 0 60 60;\" xml:space=\"preserve\">%0D%0A<style type=\"text/css\">%0D%0A%09.st0{fill:%23565656;}%0D%0A<\\/style>%0D%0A<g>%0D%0A%09<path class=\"st0\" d=\"M43.9,32.1V17.7c0-0.4-0.2-0.7-0.5-0.9l-12.8-8c-0.2-0.1-0.4-0.2-0.6-0.2c-0.2,0-0.4,0.1-0.6,0.2l-12.8,8%0D%0A%09%09c-0.3,0.2-0.5,0.5-0.5,0.9v14.4c0,0.4,0.2,0.7,0.5,0.9l12.8,8c0.3,0.2,0.8,0.2,1.1,0l12.8-8C43.7,32.8,43.9,32.5,43.9,32.1z%0D%0A%09%09 M28.9,38.2l-10.7-6.7v-12l10.7,6.7V38.2z M30,24.4l-10.8-6.8L30,10.9l10.8,6.8L30,24.4z M41.8,31.5l-10.7,6.7v-12l10.7-6.7V31.5z\"%0D%0A%09%09/>%0D%0A%09<path class=\"st0\" d=\"M56.1,29.3l-10.3-6.8v2l1.4,1v0l0.8,0.5l0,0l7,4.8c0.3,0.2,0.4,0.5,0.4,0.8c0,0.3-0.2,0.6-0.4,0.8l-6.8,4.7%0D%0A%09%09l0,0l-0.9,0.7v0l-2.9,2v0l-13.8,9.6c-0.3,0.2-0.8,0.2-1.1,0l-14.2-9.8l-2.6-1.8v0L5,32.4c-0.3-0.2-0.4-0.5-0.4-0.8%0D%0A%09%09c0-0.3,0.2-0.6,0.4-0.8l0,0l7.3-5l0,0l0.5-0.3v0l1.4-1v-2L3.9,29.3c-0.8,0.5-1.2,1.4-1.2,2.4c0,0.9,0.5,1.8,1.2,2.4l8.8,6.1v0%0D%0A%09%09l4.5,3.1l11.2,7.7c1,0.7,2.3,0.7,3.3,0l13.7-9.5l1.9-1.3v0l8.8-6.1c0.8-0.5,1.2-1.4,1.2-2.4S56.9,29.8,56.1,29.3z\"/>%0D%0A</g>%0D%0A</svg>%0D%0A');border:none;vertical-align:middle;fill:var(--smarterplan-primary)}.btn-3d:hover:enabled .icon-go-to-3d{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"utf-8\"?>%0D%0A<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->%0D%0A<svg version=\"1.1\" id=\"Calque_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"%0D%0A%09 viewBox=\"0 0 60 60\" style=\"enable-background:new 0 0 60 60;\" xml:space=\"preserve\">%0D%0A<style type=\"text/css\">%0D%0A%09.st0{fill:%23FFFFFF;}%0D%0A<\\/style>%0D%0A<g>%0D%0A%09<path class=\"st0\" d=\"M43.9,32.1V17.7c0-0.4-0.2-0.7-0.5-0.9l-12.8-8c-0.2-0.1-0.4-0.2-0.6-0.2c-0.2,0-0.4,0.1-0.6,0.2l-12.8,8%0D%0A%09%09c-0.3,0.2-0.5,0.5-0.5,0.9v14.4c0,0.4,0.2,0.7,0.5,0.9l12.8,8c0.3,0.2,0.8,0.2,1.1,0l12.8-8C43.7,32.8,43.9,32.5,43.9,32.1z%0D%0A%09%09 M28.9,38.2l-10.7-6.7v-12l10.7,6.7V38.2z M30,24.4l-10.8-6.8L30,10.9l10.8,6.8L30,24.4z M41.8,31.5l-10.7,6.7v-12l10.7-6.7V31.5z\"%0D%0A%09%09/>%0D%0A%09<path class=\"st0\" d=\"M56.1,29.3l-10.3-6.8v2l1.4,1v0l0.8,0.5l0,0l7,4.8c0.3,0.2,0.4,0.5,0.4,0.8c0,0.3-0.2,0.6-0.4,0.8l-6.8,4.7%0D%0A%09%09l0,0l-0.9,0.7v0l-2.9,2v0l-13.8,9.6c-0.3,0.2-0.8,0.2-1.1,0l-14.2-9.8l-2.6-1.8v0L5,32.4c-0.3-0.2-0.4-0.5-0.4-0.8%0D%0A%09%09c0-0.3,0.2-0.6,0.4-0.8l0,0l7.3-5l0,0l0.5-0.3v0l1.4-1v-2L3.9,29.3c-0.8,0.5-1.2,1.4-1.2,2.4c0,0.9,0.5,1.8,1.2,2.4l8.8,6.1v0%0D%0A%09%09l4.5,3.1l11.2,7.7c1,0.7,2.3,0.7,3.3,0l13.7-9.5l1.9-1.3v0l8.8-6.1c0.8-0.5,1.2-1.4,1.2-2.4S56.9,29.8,56.1,29.3z\"/>%0D%0A</g>%0D%0A</svg>%0D%0A')}.btn-3d[disabled]:hover .icon-go-to-3d{fill:var(--smarterplan-primary)}.outer_circle{background-color:#ebebeb;border-radius:50%;width:32px;height:32px;padding:1px}@media (max-width: 767px){.navbar{padding:.2rem .5rem}.form-control,.location,.floor,.zone{max-width:100%;margin-bottom:15px}input,select{margin-right:0;margin-bottom:15px}}\n"] }]
14472
+ args: [{ selector: 'lib-navigation-bar', template: "<nav class=\"navbar navbar-expand-md navbar-dark navbar-inverse second-nav\">\n <div class=\"d-flex w-100\">\n <div #navbarSelectMenu class=\"w-100\" id=\"nav-item-container-blue\">\n <div class=\"d-flex flex-column flex-md-row p-1 mt-2\">\n <select class=\"form-control flex-fill flex-lg-fill-unset location select-menu\"\n [(ngModel)]=\"selectedLocation\" (ngModelChange)=\"onLocationClick()\">\n <option [ngValue]=\"null\" selected>{{ 'Location' | translate }}</option>\n <option *ngFor=\"let loc of locations; index as index\" [ngValue]=\"loc\"> {{ loc.name }}</option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu\" (change)=\"onVisitClick($event)\"\n *ngIf=\"visitsPerSpace && visitsPerSpace.length > 1\">\n <option value=\"default\" selected disabled hidden>{{'Visit' | translate}}</option>\n <option *ngFor=\"let visit of visitsPerSpace; index as index\" value=\"{{index}}\">{{ visit.name}}\n </option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset floor select-menu\" [(ngModel)]=\"selectedFloor\"\n (ngModelChange)=\"onFloorClick()\">\n <option [ngValue]=\"null\" selected>{{'Floor' | translate}}</option>\n <option *ngFor=\"let floor of floorsPerSpace\" [ngValue]=\"floor\">{{ floor.name}}</option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu zone\" [(ngModel)]=\"selectedZone\"\n (ngModelChange)=\"onZoneClick()\">\n <option [ngValue]=\"null\" selected>{{'Zone' | translate}}</option>\n <option *ngFor=\"let zone of zonesPerFloor\" [ngValue]=\"zone\"><span\n *ngIf=\"zone.code_ref\">{{zone.code_ref}}_</span>{{zone.name}}</option>\n </select>\n <select class=\"form-control flex-fill flex-lg-fill-unset select-menu\" [(ngModel)]=\"selectedChildZone\"\n *ngIf=\"childrenZones && childrenZones.length>0\" (ngModelChange)=\"onChildZoneClick()\">\n <option [ngValue]=\"null\" selected disabled>{{'Zone' | translate}}</option>\n <option *ngFor=\"let zone of childrenZones\" [ngValue]=\"zone\">{{ zone.name}}</option>\n </select>\n <button (click)=\"goTo()\"\n class=\"btn btn-outline-primary rounded-pill mx-2 w-md-fit-content h-md-fit-content align-self-md-center btn-3d d-none d-lg-block\"\n [disabled]=\"!selectedLocation || !selectedModel3d || !canGo()\">\n <i class=\"icon-go-to-3d d-inline-block\"></i> {{'3D visit' | translate}}\n </button>\n </div>\n <div class=\"row align-items-center justify-content-center ml-0 p-1\" *ngIf=\"showLocation()\">\n <p class=\"fw-bold lh-base align-middle text-center\">\n {{ showLocation() }}\n </p>\n </div>\n <div class=\"row align-items-center justify-content-center ml-0 p-1 d-block d-lg-none\">\n <button (click)=\"goTo()\"\n class=\"btn btn-outline-primary rounded-pill w-25 mx-2 w-md-fit-content h-md-fit-content align-self-md-center btn-3d\"\n [disabled]=\"!selectedLocation || !selectedModel3d || !canGo()\">\n <i class=\"icon-go-to-3d d-inline-block\"></i> {{'3D visit' | translate}}\n </button>\n </div>\n </div>\n </div>\n</nav>", styles: [":host{position:sticky;top:0;height:123px}.navbar{z-index:200;padding:.4rem 1rem}.navbar.second-nav{z-index:199;background-color:var(--smarterplan-primary)}input,select{margin-right:15px}.select{background:transparent;border:none;color:#fff;margin:6px 20px 0}@media (min-width: 992px){.flex-lg-fill-unset{flex:unset!important}}.form-control{width:100%;min-width:80px;max-width:18%}.location{width:auto;max-width:35%}.floor,.zone{width:auto;max-width:30%}button{color:#fff;padding:5px 10px;font-size:1.1em}.navbar-toggler{margin-right:1em}.select-menu{max-width:inherit}.btn-3d .icon-go-to-3d{width:30px;height:30px;background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"utf-8\"?>%0A<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->%0A<svg version=\"1.1\" id=\"Calque_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"%0A%09 viewBox=\"0 0 60 60\" style=\"enable-background:new 0 0 60 60;\" xml:space=\"preserve\">%0A<style type=\"text/css\">%0A%09.st0{fill:%23565656;}%0A<\\/style>%0A<g>%0A%09<path class=\"st0\" d=\"M43.9,32.1V17.7c0-0.4-0.2-0.7-0.5-0.9l-12.8-8c-0.2-0.1-0.4-0.2-0.6-0.2c-0.2,0-0.4,0.1-0.6,0.2l-12.8,8%0A%09%09c-0.3,0.2-0.5,0.5-0.5,0.9v14.4c0,0.4,0.2,0.7,0.5,0.9l12.8,8c0.3,0.2,0.8,0.2,1.1,0l12.8-8C43.7,32.8,43.9,32.5,43.9,32.1z%0A%09%09 M28.9,38.2l-10.7-6.7v-12l10.7,6.7V38.2z M30,24.4l-10.8-6.8L30,10.9l10.8,6.8L30,24.4z M41.8,31.5l-10.7,6.7v-12l10.7-6.7V31.5z\"%0A%09%09/>%0A%09<path class=\"st0\" d=\"M56.1,29.3l-10.3-6.8v2l1.4,1v0l0.8,0.5l0,0l7,4.8c0.3,0.2,0.4,0.5,0.4,0.8c0,0.3-0.2,0.6-0.4,0.8l-6.8,4.7%0A%09%09l0,0l-0.9,0.7v0l-2.9,2v0l-13.8,9.6c-0.3,0.2-0.8,0.2-1.1,0l-14.2-9.8l-2.6-1.8v0L5,32.4c-0.3-0.2-0.4-0.5-0.4-0.8%0A%09%09c0-0.3,0.2-0.6,0.4-0.8l0,0l7.3-5l0,0l0.5-0.3v0l1.4-1v-2L3.9,29.3c-0.8,0.5-1.2,1.4-1.2,2.4c0,0.9,0.5,1.8,1.2,2.4l8.8,6.1v0%0A%09%09l4.5,3.1l11.2,7.7c1,0.7,2.3,0.7,3.3,0l13.7-9.5l1.9-1.3v0l8.8-6.1c0.8-0.5,1.2-1.4,1.2-2.4S56.9,29.8,56.1,29.3z\"/>%0A</g>%0A</svg>%0A');border:none;vertical-align:middle;fill:var(--smarterplan-primary)}.btn-3d:hover:enabled .icon-go-to-3d{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"utf-8\"?>%0A<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->%0A<svg version=\"1.1\" id=\"Calque_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"%0A%09 viewBox=\"0 0 60 60\" style=\"enable-background:new 0 0 60 60;\" xml:space=\"preserve\">%0A<style type=\"text/css\">%0A%09.st0{fill:%23FFFFFF;}%0A<\\/style>%0A<g>%0A%09<path class=\"st0\" d=\"M43.9,32.1V17.7c0-0.4-0.2-0.7-0.5-0.9l-12.8-8c-0.2-0.1-0.4-0.2-0.6-0.2c-0.2,0-0.4,0.1-0.6,0.2l-12.8,8%0A%09%09c-0.3,0.2-0.5,0.5-0.5,0.9v14.4c0,0.4,0.2,0.7,0.5,0.9l12.8,8c0.3,0.2,0.8,0.2,1.1,0l12.8-8C43.7,32.8,43.9,32.5,43.9,32.1z%0A%09%09 M28.9,38.2l-10.7-6.7v-12l10.7,6.7V38.2z M30,24.4l-10.8-6.8L30,10.9l10.8,6.8L30,24.4z M41.8,31.5l-10.7,6.7v-12l10.7-6.7V31.5z\"%0A%09%09/>%0A%09<path class=\"st0\" d=\"M56.1,29.3l-10.3-6.8v2l1.4,1v0l0.8,0.5l0,0l7,4.8c0.3,0.2,0.4,0.5,0.4,0.8c0,0.3-0.2,0.6-0.4,0.8l-6.8,4.7%0A%09%09l0,0l-0.9,0.7v0l-2.9,2v0l-13.8,9.6c-0.3,0.2-0.8,0.2-1.1,0l-14.2-9.8l-2.6-1.8v0L5,32.4c-0.3-0.2-0.4-0.5-0.4-0.8%0A%09%09c0-0.3,0.2-0.6,0.4-0.8l0,0l7.3-5l0,0l0.5-0.3v0l1.4-1v-2L3.9,29.3c-0.8,0.5-1.2,1.4-1.2,2.4c0,0.9,0.5,1.8,1.2,2.4l8.8,6.1v0%0A%09%09l4.5,3.1l11.2,7.7c1,0.7,2.3,0.7,3.3,0l13.7-9.5l1.9-1.3v0l8.8-6.1c0.8-0.5,1.2-1.4,1.2-2.4S56.9,29.8,56.1,29.3z\"/>%0A</g>%0A</svg>%0A')}.btn-3d[disabled]:hover .icon-go-to-3d{fill:var(--smarterplan-primary)}.outer_circle{background-color:#ebebeb;border-radius:50%;width:32px;height:32px;padding:1px}@media (max-width: 767px){.navbar{padding:.2rem .5rem}.form-control,.location,.floor,.zone{max-width:100%;margin-bottom:15px}input,select{margin-right:0;margin-bottom:15px}}\n"] }]
12820
14473
  }], ctorParameters: () => [{ type: undefined, decorators: [{
12821
14474
  type: Inject,
12822
14475
  args: ['currentAwsKinesisAnalyticsService']
@@ -13085,11 +14738,11 @@ class MenuBarComponent {
13085
14738
  }
13086
14739
  }
13087
14740
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MenuBarComponent, deps: [{ token: i1$1.Router }, { token: LocaleService }, { token: BaseUserService }, { token: NavigatorService }, { token: SupportService }, { token: BaseVisibilityService }, { token: i1$2.NgbCalendar }, { token: i1$2.NgbDateParserFormatter }, { token: FilterService }, { token: AmplifyCacheService }], target: i0.ɵɵFactoryTarget.Component });
13088
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: MenuBarComponent, selector: "lib-menu-bar", inputs: { cacheName: "cacheName" }, ngImport: i0, template: "<div id=\"menu-bar\">\r\n <div *ngIf=\"!isVisitor\">\r\n <nav class=\"navbar navbar-expand-md navbar-light p-0\">\r\n <div class=\"container-fluid\" *ngIf=\"isVisible\">\r\n <div class=\"navbar-brand order-md-0\" [routerLink]=\"['/dashboard']\">\r\n <img alt=\"SmarterPlan\" src=\"../assets/images/logos/logo-immo.png\">\r\n </div>\r\n <div class=\"ms-auto nav-item dropdown profile_pic-container order-md-2\">\r\n <div class=\"profile_pic dropdown-toggle\" data-testid=\"dropdown-menu-button\" data-bs-toggle=\"dropdown\">\r\n <lib-avatar></lib-avatar>\r\n </div>\r\n <div class=\"dropdown-menu dropdown-menu-right\">\r\n <p *ngIf=\"user && mission\" class=\"p-2\">\r\n <span class=\"username\">{{ user.displayName }}</span><br />\r\n <span class=\"user-role\">{{ mission.role }}</span><br />\r\n <span class=\"org\">{{ mission.organisation ? mission.organisation.name : \"No organisation\" |\r\n translate}}</span>\r\n </p>\r\n <div class=\"dropdown-divider\"></div>\r\n <!-- <a class=\"dropdown-item\" routerLink=\"/debug\" *ngIf=\"isManager\">Debug</a> -->\r\n <a class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</a>\r\n <a class=\"dropdown-item\" routerLink=\"/dashboard\">{{'Data management' | translate}}</a>\r\n <a class=\"dropdown-item\" data-testid=\"user-management-btn\" routerLink=\"/admin\" *ngIf=\"isManager\">\r\n {{'Users management' | translate}}</a>\r\n <a class=\"dropdown-item\" routerLink=\"/profile\">\r\n {{'Profile' | translate}}</a>\r\n <div class=\"dropdown-divider\"></div>\r\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\r\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\r\n translate}}</button>\r\n </div>\r\n </div>\r\n <button class=\"navbar-toggler order-md-1\" (click)=\"navbarCollapse.toggle()\" type=\"button\"\r\n [attr.aria-expanded]=\"!isCollapsed\" aria-controls=\"navbar-collapse\">\r\n <span class=\"navbar-toggler-icon\"></span>\r\n </button>\r\n <div class=\"navbar-collapse order-md-1\" #navbarCollapse=\"ngbCollapse\" [(ngbCollapse)]=\"isCollapsed\"\r\n [animation]=\"false\">\r\n <form\r\n class=\"form-inline m-0 mt-2 form-container d-flex justify-content-start align-items-start align-items-md-center flex-column flex-md-row flex-nowrap filters-menu-block\">\r\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\r\n <input class=\"form-control search-button m-1\" placeholder=\"keyword\" id=\"mainSearchBar\" type=\"text\">\r\n <button class=\"btn btn-outline-primary rounded-pill m-1 w-fit-content h-fit-content align-self-md-center\"\r\n (click)=\"onSubmit()\">{{'Search' |\r\n translate}}</button>\r\n </div>\r\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\r\n <div class=\"input-group date-selector m-1\">\r\n <input #dpFromDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpFromDate\"\r\n [value]=\"fromDate | formatDateNumberToDigits\"\r\n (input)=\"fromDate = validateInput(fromDate, dpFromDate.value)\">\r\n <div class=\"input-group-append\">\r\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\r\n type=\"button\"></button>\r\n </div>\r\n </div>\r\n <input name=\"datepicker\" class=\"form-control visually-hidden\" ngbDatepicker #datepicker=\"ngbDatepicker\"\r\n [autoClose]=\"'outside'\" (dateSelect)=\"onDateSelection($event)\" [displayMonths]=\"2\" [dayTemplate]=\"t\"\r\n outsideDays=\"hidden\" [startDate]=\"fromDate || calendar.getToday()\" [container]=\"'body'\" tabindex=\"-1\">\r\n <ng-template #t let-date let-focused=\"focused\">\r\n <span class=\"custom-day\" [class.focused]=\"focused\" [class.range]=\"isRange(date)\"\r\n [class.faded]=\"isHovered(date) || isInside(date)\" (mouseenter)=\"hoveredDate = date\"\r\n (mouseleave)=\"hoveredDate = null\">\r\n {{ date.day }}\r\n </span>\r\n </ng-template>\r\n <span class=\"ms-1 p-2\">{{'to' | translate}}</span>\r\n <div class=\"input-group date-selector m-1\">\r\n <input #dpToDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpToDate\"\r\n [value]=\"toDate | formatDateNumberToDigits\" (input)=\"toDate = validateInput(toDate, dpToDate.value)\">\r\n <div class=\"input-group-append\">\r\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\r\n type=\"button\"></button>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-1 justify-content-evenly\">\r\n <button\r\n class=\"btn btn-primary rounded-pill ms-1 w-25 w-md-fit-content h-md-fit-content align-self-md-center\"\r\n type=\"button\" (click)=\"onApply()\" *ngIf=\"!isDateFilterApplied || dateChanged\">\r\n {{'Apply' | translate}}\r\n </button>\r\n <span\r\n class=\"appliedText ms-1 py-2 align-self-md-center pb-md-0 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\r\n *ngIf=\"isDateFilterApplied && !dateChanged\">\r\n <span class=\"iconify\" data-icon=\"ion:checkmark\" data-width=\"15\" data-height=\"15\"></span>\r\n {{\"Applied\" | translate}}\r\n </span>\r\n <button\r\n class=\"btn btn-outline-primary rounded-pill w-25 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\r\n type=\"button\" (click)=\"onClearDates()\">{{'Clear'\r\n |\r\n translate}}</button>\r\n <div\r\n class=\"change-position-button align-self-end pb-2 align-self-md-center mx-2 pb-md-0 flex-nowrap w-25 w-md-fit-content h-md-fit-content\">\r\n <input type=\"checkbox\" class=\"btn-check\" id=\"btn-check-outlined\" autocomplete=\"off\">\r\n <label class=\"link-primary cursor-pointer text-decoration-underline\" (click)=\"onChangePositionClick()\"\r\n for=\"btn-check-outlined\">{{\r\n (isChangePositionVisible ? \"Close\" : \"Move to\") | translate }}</label>\r\n </div>\r\n </div>\r\n </form>\r\n </div>\r\n </div>\r\n </nav>\r\n <lib-navigation-bar *ngIf=\"isVisible && isChangePositionVisible\"></lib-navigation-bar>\r\n </div>\r\n <div *ngIf=\"isVisitor && isVisible\">\r\n <div class=\"visitor-div\">\r\n <div class=\"logo-container\" data-testid=\"dropdown-menu-button\">\r\n <img class=\"img-logo\" alt=\"SmarterPlanLogo\" src=\"../assets/images/logos/logo-reserve.png\">\r\n </div>\r\n <div class=\"avatar-container-menu\">\r\n <div class=\"profile_pic dropdown-toggle cursor-pointer\" data-bs-toggle=\"dropdown\">\r\n <lib-avatar></lib-avatar>\r\n </div>\r\n <div class=\"dropdown-menu dropdown-menu-right\">\r\n <button class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</button>\r\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\r\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\r\n translate}}</button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>", styles: ["#menu-bar{position:sticky;top:0;z-index:200}.navbar{z-index:200}.navbar-brand{padding-top:5px;padding-bottom:5px;cursor:pointer}.navbar-brand img{height:70px}.profile_pic{border-radius:50%;height:45px;width:45px;margin:8px;border:.2px solid rgb(150,150,150)}.navbar-nav .profile-pic-container .dropdown-menu{position:absolute}.navbar.second-nav{z-index:199;background-color:var(--smarterplan-primary)}.profile_pic.dropdown-toggle:after{display:none}.navbar-light{border-bottom:1px solid var(--smarterplan-primary);background-color:#fff}.navbar form{width:100%;margin:0;padding:0}input,select{margin-right:15px}.select{background:transparent;border:none;color:#fff;margin:6px 20px 0;width:auto}button{padding:5px 10px}button:disabled,button[disabled]{color:#c04f4f}.navbar-toggler{margin-right:1em}.icon{height:40px;width:40px;color:var(--smarterplan-primary)}.search-button-container{width:auto;align-items:stretch;justify-items:start}.search-button-container .search-button{width:auto;border-radius:1rem!important}.visitor-div{background-color:var(--smarterplan-primary);height:60px;display:flex;justify-content:space-between}.profile_pic-container{margin-right:1em}.logo-container,.img-logo{height:100%}.change-position-button{margin-left:2rem}.change-position-button label,.change-position-button .btn-check:checked,.change-position-button .btn-check:focus,.change-position-button .btn-outline-primary,.change-position-button .btn{box-shadow:none!important;outline:0}.change-position-button label{border-radius:16px}.change-position-button .btn-check:checked+.btn{background-color:var(--smarterplan-primary);color:#fff!important}.filters-menu-block{width:auto}@media (min-width: 1200px){.navbar-brand img{height:100px}.profile_pic{height:60px;width:60px;margin:15px}}@media (min-width: 992px) and (max-width: 1199.98px){.navbar-brand img{height:80px}.profile_pic{height:50px;width:50px;margin:12px}}@media (min-width: 768px) and (max-width: 991.98px){.navbar-brand img{height:60px}.profile_pic{height:40px;width:40px;margin:10px}}@media (max-width: 767.98px){.navbar-brand img{height:50px}.profile_pic{height:30px;width:30px;margin:8px}.search-button-container,.filters-menu-block{width:100%}}.form-container{margin:.5rem 0}.navbar-collapse.show{display:flex;padding-bottom:.5rem}.hidden{width:0;margin:0;border:none;padding:0}.custom-day{text-align:center;padding:.185rem .25rem;display:inline-block;height:2rem;width:2rem}.custom-day.focused{background-color:#e6e6e6}.custom-day.range,.custom-day:hover{background-color:var(--smarterplan-primary-lighter);color:#fff}.custom-day.faded{background-color:var(--smarterplan-primary-transparent)}.calendar{height:100%}.appliedText,.appliedText span{color:var(--smarterplan-primary)}@media (max-width: 767px){.form-inline{flex-direction:column}}.hidden{opacity:0}.date-selector{flex-wrap:nowrap}.date-selector input{min-width:80px;margin:0}@media (min-width: 768px){.w-md-fit-content{width:fit-content!important}.h-md-content{height:fit-content!important}}\n"], dependencies: [{ kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i1$2.NgbCollapse, selector: "[ngbCollapse]", inputs: ["animation", "ngbCollapse", "horizontal"], outputs: ["ngbCollapseChange", "shown", "hidden"], exportAs: ["ngbCollapse"] }, { kind: "directive", type: i1$2.NgbInputDatepicker, selector: "input[ngbDatepicker]", inputs: ["autoClose", "contentTemplate", "datepickerClass", "dayTemplate", "dayTemplateData", "displayMonths", "firstDayOfWeek", "footerTemplate", "markDisabled", "minDate", "maxDate", "navigation", "outsideDays", "placement", "popperOptions", "restoreFocus", "showWeekNumbers", "startDate", "container", "positionTarget", "weekdays", "disabled"], outputs: ["dateSelect", "navigate", "closed"], exportAs: ["ngbDatepicker"] }, { kind: "component", type: AvatarComponent, selector: "lib-avatar", inputs: ["size", "userID"] }, { kind: "component", type: NavigationBarComponent, selector: "lib-navigation-bar" }, { kind: "pipe", type: i1.TranslatePipe, name: "translate" }, { kind: "pipe", type: FormatDateNumberToDigitsPipe, name: "formatDateNumberToDigits" }] });
14741
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: MenuBarComponent, selector: "lib-menu-bar", inputs: { cacheName: "cacheName" }, ngImport: i0, template: "<div id=\"menu-bar\">\n <div *ngIf=\"!isVisitor\">\n <nav class=\"navbar navbar-expand-md navbar-light p-0\">\n <div class=\"container-fluid\" *ngIf=\"isVisible\">\n <div class=\"navbar-brand order-md-0\" [routerLink]=\"['/dashboard']\">\n <img alt=\"SmarterPlan\" src=\"../assets/images/logos/logo-immo.png\">\n </div>\n <div class=\"ms-auto nav-item dropdown profile_pic-container order-md-2\">\n <div class=\"profile_pic dropdown-toggle\" data-testid=\"dropdown-menu-button\" data-bs-toggle=\"dropdown\">\n <lib-avatar></lib-avatar>\n </div>\n <div class=\"dropdown-menu dropdown-menu-right\">\n <p *ngIf=\"user && mission\" class=\"p-2\">\n <span class=\"username\">{{ user.displayName }}</span><br />\n <span class=\"user-role\">{{ mission.role }}</span><br />\n <span class=\"org\">{{ mission.organisation ? mission.organisation.name : \"No organisation\" |\n translate}}</span>\n </p>\n <div class=\"dropdown-divider\"></div>\n <!-- <a class=\"dropdown-item\" routerLink=\"/debug\" *ngIf=\"isManager\">Debug</a> -->\n <a class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</a>\n <a class=\"dropdown-item\" routerLink=\"/dashboard\">{{'Data management' | translate}}</a>\n <a class=\"dropdown-item\" data-testid=\"user-management-btn\" routerLink=\"/admin\" *ngIf=\"isManager\">\n {{'Users management' | translate}}</a>\n <a class=\"dropdown-item\" routerLink=\"/profile\">\n {{'Profile' | translate}}</a>\n <div class=\"dropdown-divider\"></div>\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\n translate}}</button>\n </div>\n </div>\n <button class=\"navbar-toggler order-md-1\" (click)=\"navbarCollapse.toggle()\" type=\"button\"\n [attr.aria-expanded]=\"!isCollapsed\" aria-controls=\"navbar-collapse\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n <div class=\"navbar-collapse order-md-1\" #navbarCollapse=\"ngbCollapse\" [(ngbCollapse)]=\"isCollapsed\"\n [animation]=\"false\">\n <form\n class=\"form-inline m-0 mt-2 form-container d-flex justify-content-start align-items-start align-items-md-center flex-column flex-md-row flex-nowrap filters-menu-block\">\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\n <input class=\"form-control search-button m-1\" placeholder=\"keyword\" id=\"mainSearchBar\" type=\"text\">\n <button class=\"btn btn-outline-primary rounded-pill m-1 w-fit-content h-fit-content align-self-md-center\"\n (click)=\"onSubmit()\">{{'Search' |\n translate}}</button>\n </div>\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\n <div class=\"input-group date-selector m-1\">\n <input #dpFromDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpFromDate\"\n [value]=\"fromDate | formatDateNumberToDigits\"\n (input)=\"fromDate = validateInput(fromDate, dpFromDate.value)\">\n <div class=\"input-group-append\">\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\n type=\"button\"></button>\n </div>\n </div>\n <input name=\"datepicker\" class=\"form-control visually-hidden\" ngbDatepicker #datepicker=\"ngbDatepicker\"\n [autoClose]=\"'outside'\" (dateSelect)=\"onDateSelection($event)\" [displayMonths]=\"2\" [dayTemplate]=\"t\"\n outsideDays=\"hidden\" [startDate]=\"fromDate || calendar.getToday()\" [container]=\"'body'\" tabindex=\"-1\">\n <ng-template #t let-date let-focused=\"focused\">\n <span class=\"custom-day\" [class.focused]=\"focused\" [class.range]=\"isRange(date)\"\n [class.faded]=\"isHovered(date) || isInside(date)\" (mouseenter)=\"hoveredDate = date\"\n (mouseleave)=\"hoveredDate = null\">\n {{ date.day }}\n </span>\n </ng-template>\n <span class=\"ms-1 p-2\">{{'to' | translate}}</span>\n <div class=\"input-group date-selector m-1\">\n <input #dpToDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpToDate\"\n [value]=\"toDate | formatDateNumberToDigits\" (input)=\"toDate = validateInput(toDate, dpToDate.value)\">\n <div class=\"input-group-append\">\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\n type=\"button\"></button>\n </div>\n </div>\n </div>\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-1 justify-content-evenly\">\n <button\n class=\"btn btn-primary rounded-pill ms-1 w-25 w-md-fit-content h-md-fit-content align-self-md-center\"\n type=\"button\" (click)=\"onApply()\" *ngIf=\"!isDateFilterApplied || dateChanged\">\n {{'Apply' | translate}}\n </button>\n <span\n class=\"appliedText ms-1 py-2 align-self-md-center pb-md-0 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\n *ngIf=\"isDateFilterApplied && !dateChanged\">\n <span class=\"iconify\" data-icon=\"ion:checkmark\" data-width=\"15\" data-height=\"15\"></span>\n {{\"Applied\" | translate}}\n </span>\n <button\n class=\"btn btn-outline-primary rounded-pill w-25 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\n type=\"button\" (click)=\"onClearDates()\">{{'Clear'\n |\n translate}}</button>\n <div\n class=\"change-position-button align-self-end pb-2 align-self-md-center mx-2 pb-md-0 flex-nowrap w-25 w-md-fit-content h-md-fit-content\">\n <input type=\"checkbox\" class=\"btn-check\" id=\"btn-check-outlined\" autocomplete=\"off\">\n <label class=\"link-primary cursor-pointer text-decoration-underline\" (click)=\"onChangePositionClick()\"\n for=\"btn-check-outlined\">{{\n (isChangePositionVisible ? \"Close\" : \"Move to\") | translate }}</label>\n </div>\n </div>\n </form>\n </div>\n </div>\n </nav>\n <lib-navigation-bar *ngIf=\"isVisible && isChangePositionVisible\"></lib-navigation-bar>\n </div>\n <div *ngIf=\"isVisitor && isVisible\">\n <div class=\"visitor-div\">\n <div class=\"logo-container\" data-testid=\"dropdown-menu-button\">\n <img class=\"img-logo\" alt=\"SmarterPlanLogo\" src=\"../assets/images/logos/logo-reserve.png\">\n </div>\n <div class=\"avatar-container-menu\">\n <div class=\"profile_pic dropdown-toggle cursor-pointer\" data-bs-toggle=\"dropdown\">\n <lib-avatar></lib-avatar>\n </div>\n <div class=\"dropdown-menu dropdown-menu-right\">\n <button class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</button>\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\n translate}}</button>\n </div>\n </div>\n </div>\n </div>\n</div>", styles: ["#menu-bar{position:sticky;top:0;z-index:200}.navbar{z-index:200}.navbar-brand{padding-top:5px;padding-bottom:5px;cursor:pointer}.navbar-brand img{height:70px}.profile_pic{border-radius:50%;height:45px;width:45px;margin:8px;border:.2px solid rgb(150,150,150)}.navbar-nav .profile-pic-container .dropdown-menu{position:absolute}.navbar.second-nav{z-index:199;background-color:var(--smarterplan-primary)}.profile_pic.dropdown-toggle:after{display:none}.navbar-light{border-bottom:1px solid var(--smarterplan-primary);background-color:#fff}.navbar form{width:100%;margin:0;padding:0}input,select{margin-right:15px}.select{background:transparent;border:none;color:#fff;margin:6px 20px 0;width:auto}button{padding:5px 10px}button:disabled,button[disabled]{color:#c04f4f}.navbar-toggler{margin-right:1em}.icon{height:40px;width:40px;color:var(--smarterplan-primary)}.search-button-container{width:auto;align-items:stretch;justify-items:start}.search-button-container .search-button{width:auto;border-radius:1rem!important}.visitor-div{background-color:var(--smarterplan-primary);height:60px;display:flex;justify-content:space-between}.profile_pic-container{margin-right:1em}.logo-container,.img-logo{height:100%}.change-position-button{margin-left:2rem}.change-position-button label,.change-position-button .btn-check:checked,.change-position-button .btn-check:focus,.change-position-button .btn-outline-primary,.change-position-button .btn{box-shadow:none!important;outline:0}.change-position-button label{border-radius:16px}.change-position-button .btn-check:checked+.btn{background-color:var(--smarterplan-primary);color:#fff!important}.filters-menu-block{width:auto}@media (min-width: 1200px){.navbar-brand img{height:100px}.profile_pic{height:60px;width:60px;margin:15px}}@media (min-width: 992px) and (max-width: 1199.98px){.navbar-brand img{height:80px}.profile_pic{height:50px;width:50px;margin:12px}}@media (min-width: 768px) and (max-width: 991.98px){.navbar-brand img{height:60px}.profile_pic{height:40px;width:40px;margin:10px}}@media (max-width: 767.98px){.navbar-brand img{height:50px}.profile_pic{height:30px;width:30px;margin:8px}.search-button-container,.filters-menu-block{width:100%}}.form-container{margin:.5rem 0}.navbar-collapse.show{display:flex;padding-bottom:.5rem}.hidden{width:0;margin:0;border:none;padding:0}.custom-day{text-align:center;padding:.185rem .25rem;display:inline-block;height:2rem;width:2rem}.custom-day.focused{background-color:#e6e6e6}.custom-day.range,.custom-day:hover{background-color:var(--smarterplan-primary-lighter);color:#fff}.custom-day.faded{background-color:var(--smarterplan-primary-transparent)}.calendar{height:100%}.appliedText,.appliedText span{color:var(--smarterplan-primary)}@media (max-width: 767px){.form-inline{flex-direction:column}}.hidden{opacity:0}.date-selector{flex-wrap:nowrap}.date-selector input{min-width:80px;margin:0}@media (min-width: 768px){.w-md-fit-content{width:fit-content!important}.h-md-content{height:fit-content!important}}\n"], dependencies: [{ kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i1$2.NgbCollapse, selector: "[ngbCollapse]", inputs: ["animation", "ngbCollapse", "horizontal"], outputs: ["ngbCollapseChange", "shown", "hidden"], exportAs: ["ngbCollapse"] }, { kind: "directive", type: i1$2.NgbInputDatepicker, selector: "input[ngbDatepicker]", inputs: ["autoClose", "contentTemplate", "datepickerClass", "dayTemplate", "dayTemplateData", "displayMonths", "firstDayOfWeek", "footerTemplate", "markDisabled", "minDate", "maxDate", "navigation", "outsideDays", "placement", "popperOptions", "restoreFocus", "showWeekNumbers", "startDate", "container", "positionTarget", "weekdays", "disabled"], outputs: ["dateSelect", "navigate", "closed"], exportAs: ["ngbDatepicker"] }, { kind: "component", type: AvatarComponent, selector: "lib-avatar", inputs: ["size", "userID"] }, { kind: "component", type: NavigationBarComponent, selector: "lib-navigation-bar" }, { kind: "pipe", type: i1.TranslatePipe, name: "translate" }, { kind: "pipe", type: FormatDateNumberToDigitsPipe, name: "formatDateNumberToDigits" }] });
13089
14742
  }
13090
14743
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MenuBarComponent, decorators: [{
13091
14744
  type: Component,
13092
- args: [{ selector: 'lib-menu-bar', template: "<div id=\"menu-bar\">\r\n <div *ngIf=\"!isVisitor\">\r\n <nav class=\"navbar navbar-expand-md navbar-light p-0\">\r\n <div class=\"container-fluid\" *ngIf=\"isVisible\">\r\n <div class=\"navbar-brand order-md-0\" [routerLink]=\"['/dashboard']\">\r\n <img alt=\"SmarterPlan\" src=\"../assets/images/logos/logo-immo.png\">\r\n </div>\r\n <div class=\"ms-auto nav-item dropdown profile_pic-container order-md-2\">\r\n <div class=\"profile_pic dropdown-toggle\" data-testid=\"dropdown-menu-button\" data-bs-toggle=\"dropdown\">\r\n <lib-avatar></lib-avatar>\r\n </div>\r\n <div class=\"dropdown-menu dropdown-menu-right\">\r\n <p *ngIf=\"user && mission\" class=\"p-2\">\r\n <span class=\"username\">{{ user.displayName }}</span><br />\r\n <span class=\"user-role\">{{ mission.role }}</span><br />\r\n <span class=\"org\">{{ mission.organisation ? mission.organisation.name : \"No organisation\" |\r\n translate}}</span>\r\n </p>\r\n <div class=\"dropdown-divider\"></div>\r\n <!-- <a class=\"dropdown-item\" routerLink=\"/debug\" *ngIf=\"isManager\">Debug</a> -->\r\n <a class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</a>\r\n <a class=\"dropdown-item\" routerLink=\"/dashboard\">{{'Data management' | translate}}</a>\r\n <a class=\"dropdown-item\" data-testid=\"user-management-btn\" routerLink=\"/admin\" *ngIf=\"isManager\">\r\n {{'Users management' | translate}}</a>\r\n <a class=\"dropdown-item\" routerLink=\"/profile\">\r\n {{'Profile' | translate}}</a>\r\n <div class=\"dropdown-divider\"></div>\r\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\r\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\r\n translate}}</button>\r\n </div>\r\n </div>\r\n <button class=\"navbar-toggler order-md-1\" (click)=\"navbarCollapse.toggle()\" type=\"button\"\r\n [attr.aria-expanded]=\"!isCollapsed\" aria-controls=\"navbar-collapse\">\r\n <span class=\"navbar-toggler-icon\"></span>\r\n </button>\r\n <div class=\"navbar-collapse order-md-1\" #navbarCollapse=\"ngbCollapse\" [(ngbCollapse)]=\"isCollapsed\"\r\n [animation]=\"false\">\r\n <form\r\n class=\"form-inline m-0 mt-2 form-container d-flex justify-content-start align-items-start align-items-md-center flex-column flex-md-row flex-nowrap filters-menu-block\">\r\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\r\n <input class=\"form-control search-button m-1\" placeholder=\"keyword\" id=\"mainSearchBar\" type=\"text\">\r\n <button class=\"btn btn-outline-primary rounded-pill m-1 w-fit-content h-fit-content align-self-md-center\"\r\n (click)=\"onSubmit()\">{{'Search' |\r\n translate}}</button>\r\n </div>\r\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\r\n <div class=\"input-group date-selector m-1\">\r\n <input #dpFromDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpFromDate\"\r\n [value]=\"fromDate | formatDateNumberToDigits\"\r\n (input)=\"fromDate = validateInput(fromDate, dpFromDate.value)\">\r\n <div class=\"input-group-append\">\r\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\r\n type=\"button\"></button>\r\n </div>\r\n </div>\r\n <input name=\"datepicker\" class=\"form-control visually-hidden\" ngbDatepicker #datepicker=\"ngbDatepicker\"\r\n [autoClose]=\"'outside'\" (dateSelect)=\"onDateSelection($event)\" [displayMonths]=\"2\" [dayTemplate]=\"t\"\r\n outsideDays=\"hidden\" [startDate]=\"fromDate || calendar.getToday()\" [container]=\"'body'\" tabindex=\"-1\">\r\n <ng-template #t let-date let-focused=\"focused\">\r\n <span class=\"custom-day\" [class.focused]=\"focused\" [class.range]=\"isRange(date)\"\r\n [class.faded]=\"isHovered(date) || isInside(date)\" (mouseenter)=\"hoveredDate = date\"\r\n (mouseleave)=\"hoveredDate = null\">\r\n {{ date.day }}\r\n </span>\r\n </ng-template>\r\n <span class=\"ms-1 p-2\">{{'to' | translate}}</span>\r\n <div class=\"input-group date-selector m-1\">\r\n <input #dpToDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpToDate\"\r\n [value]=\"toDate | formatDateNumberToDigits\" (input)=\"toDate = validateInput(toDate, dpToDate.value)\">\r\n <div class=\"input-group-append\">\r\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\r\n type=\"button\"></button>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-1 justify-content-evenly\">\r\n <button\r\n class=\"btn btn-primary rounded-pill ms-1 w-25 w-md-fit-content h-md-fit-content align-self-md-center\"\r\n type=\"button\" (click)=\"onApply()\" *ngIf=\"!isDateFilterApplied || dateChanged\">\r\n {{'Apply' | translate}}\r\n </button>\r\n <span\r\n class=\"appliedText ms-1 py-2 align-self-md-center pb-md-0 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\r\n *ngIf=\"isDateFilterApplied && !dateChanged\">\r\n <span class=\"iconify\" data-icon=\"ion:checkmark\" data-width=\"15\" data-height=\"15\"></span>\r\n {{\"Applied\" | translate}}\r\n </span>\r\n <button\r\n class=\"btn btn-outline-primary rounded-pill w-25 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\r\n type=\"button\" (click)=\"onClearDates()\">{{'Clear'\r\n |\r\n translate}}</button>\r\n <div\r\n class=\"change-position-button align-self-end pb-2 align-self-md-center mx-2 pb-md-0 flex-nowrap w-25 w-md-fit-content h-md-fit-content\">\r\n <input type=\"checkbox\" class=\"btn-check\" id=\"btn-check-outlined\" autocomplete=\"off\">\r\n <label class=\"link-primary cursor-pointer text-decoration-underline\" (click)=\"onChangePositionClick()\"\r\n for=\"btn-check-outlined\">{{\r\n (isChangePositionVisible ? \"Close\" : \"Move to\") | translate }}</label>\r\n </div>\r\n </div>\r\n </form>\r\n </div>\r\n </div>\r\n </nav>\r\n <lib-navigation-bar *ngIf=\"isVisible && isChangePositionVisible\"></lib-navigation-bar>\r\n </div>\r\n <div *ngIf=\"isVisitor && isVisible\">\r\n <div class=\"visitor-div\">\r\n <div class=\"logo-container\" data-testid=\"dropdown-menu-button\">\r\n <img class=\"img-logo\" alt=\"SmarterPlanLogo\" src=\"../assets/images/logos/logo-reserve.png\">\r\n </div>\r\n <div class=\"avatar-container-menu\">\r\n <div class=\"profile_pic dropdown-toggle cursor-pointer\" data-bs-toggle=\"dropdown\">\r\n <lib-avatar></lib-avatar>\r\n </div>\r\n <div class=\"dropdown-menu dropdown-menu-right\">\r\n <button class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</button>\r\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\r\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\r\n translate}}</button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>", styles: ["#menu-bar{position:sticky;top:0;z-index:200}.navbar{z-index:200}.navbar-brand{padding-top:5px;padding-bottom:5px;cursor:pointer}.navbar-brand img{height:70px}.profile_pic{border-radius:50%;height:45px;width:45px;margin:8px;border:.2px solid rgb(150,150,150)}.navbar-nav .profile-pic-container .dropdown-menu{position:absolute}.navbar.second-nav{z-index:199;background-color:var(--smarterplan-primary)}.profile_pic.dropdown-toggle:after{display:none}.navbar-light{border-bottom:1px solid var(--smarterplan-primary);background-color:#fff}.navbar form{width:100%;margin:0;padding:0}input,select{margin-right:15px}.select{background:transparent;border:none;color:#fff;margin:6px 20px 0;width:auto}button{padding:5px 10px}button:disabled,button[disabled]{color:#c04f4f}.navbar-toggler{margin-right:1em}.icon{height:40px;width:40px;color:var(--smarterplan-primary)}.search-button-container{width:auto;align-items:stretch;justify-items:start}.search-button-container .search-button{width:auto;border-radius:1rem!important}.visitor-div{background-color:var(--smarterplan-primary);height:60px;display:flex;justify-content:space-between}.profile_pic-container{margin-right:1em}.logo-container,.img-logo{height:100%}.change-position-button{margin-left:2rem}.change-position-button label,.change-position-button .btn-check:checked,.change-position-button .btn-check:focus,.change-position-button .btn-outline-primary,.change-position-button .btn{box-shadow:none!important;outline:0}.change-position-button label{border-radius:16px}.change-position-button .btn-check:checked+.btn{background-color:var(--smarterplan-primary);color:#fff!important}.filters-menu-block{width:auto}@media (min-width: 1200px){.navbar-brand img{height:100px}.profile_pic{height:60px;width:60px;margin:15px}}@media (min-width: 992px) and (max-width: 1199.98px){.navbar-brand img{height:80px}.profile_pic{height:50px;width:50px;margin:12px}}@media (min-width: 768px) and (max-width: 991.98px){.navbar-brand img{height:60px}.profile_pic{height:40px;width:40px;margin:10px}}@media (max-width: 767.98px){.navbar-brand img{height:50px}.profile_pic{height:30px;width:30px;margin:8px}.search-button-container,.filters-menu-block{width:100%}}.form-container{margin:.5rem 0}.navbar-collapse.show{display:flex;padding-bottom:.5rem}.hidden{width:0;margin:0;border:none;padding:0}.custom-day{text-align:center;padding:.185rem .25rem;display:inline-block;height:2rem;width:2rem}.custom-day.focused{background-color:#e6e6e6}.custom-day.range,.custom-day:hover{background-color:var(--smarterplan-primary-lighter);color:#fff}.custom-day.faded{background-color:var(--smarterplan-primary-transparent)}.calendar{height:100%}.appliedText,.appliedText span{color:var(--smarterplan-primary)}@media (max-width: 767px){.form-inline{flex-direction:column}}.hidden{opacity:0}.date-selector{flex-wrap:nowrap}.date-selector input{min-width:80px;margin:0}@media (min-width: 768px){.w-md-fit-content{width:fit-content!important}.h-md-content{height:fit-content!important}}\n"] }]
14745
+ args: [{ selector: 'lib-menu-bar', template: "<div id=\"menu-bar\">\n <div *ngIf=\"!isVisitor\">\n <nav class=\"navbar navbar-expand-md navbar-light p-0\">\n <div class=\"container-fluid\" *ngIf=\"isVisible\">\n <div class=\"navbar-brand order-md-0\" [routerLink]=\"['/dashboard']\">\n <img alt=\"SmarterPlan\" src=\"../assets/images/logos/logo-immo.png\">\n </div>\n <div class=\"ms-auto nav-item dropdown profile_pic-container order-md-2\">\n <div class=\"profile_pic dropdown-toggle\" data-testid=\"dropdown-menu-button\" data-bs-toggle=\"dropdown\">\n <lib-avatar></lib-avatar>\n </div>\n <div class=\"dropdown-menu dropdown-menu-right\">\n <p *ngIf=\"user && mission\" class=\"p-2\">\n <span class=\"username\">{{ user.displayName }}</span><br />\n <span class=\"user-role\">{{ mission.role }}</span><br />\n <span class=\"org\">{{ mission.organisation ? mission.organisation.name : \"No organisation\" |\n translate}}</span>\n </p>\n <div class=\"dropdown-divider\"></div>\n <!-- <a class=\"dropdown-item\" routerLink=\"/debug\" *ngIf=\"isManager\">Debug</a> -->\n <a class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</a>\n <a class=\"dropdown-item\" routerLink=\"/dashboard\">{{'Data management' | translate}}</a>\n <a class=\"dropdown-item\" data-testid=\"user-management-btn\" routerLink=\"/admin\" *ngIf=\"isManager\">\n {{'Users management' | translate}}</a>\n <a class=\"dropdown-item\" routerLink=\"/profile\">\n {{'Profile' | translate}}</a>\n <div class=\"dropdown-divider\"></div>\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\n translate}}</button>\n </div>\n </div>\n <button class=\"navbar-toggler order-md-1\" (click)=\"navbarCollapse.toggle()\" type=\"button\"\n [attr.aria-expanded]=\"!isCollapsed\" aria-controls=\"navbar-collapse\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n <div class=\"navbar-collapse order-md-1\" #navbarCollapse=\"ngbCollapse\" [(ngbCollapse)]=\"isCollapsed\"\n [animation]=\"false\">\n <form\n class=\"form-inline m-0 mt-2 form-container d-flex justify-content-start align-items-start align-items-md-center flex-column flex-md-row flex-nowrap filters-menu-block\">\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\n <input class=\"form-control search-button m-1\" placeholder=\"keyword\" id=\"mainSearchBar\" type=\"text\">\n <button class=\"btn btn-outline-primary rounded-pill m-1 w-fit-content h-fit-content align-self-md-center\"\n (click)=\"onSubmit()\">{{'Search' |\n translate}}</button>\n </div>\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-0\">\n <div class=\"input-group date-selector m-1\">\n <input #dpFromDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpFromDate\"\n [value]=\"fromDate | formatDateNumberToDigits\"\n (input)=\"fromDate = validateInput(fromDate, dpFromDate.value)\">\n <div class=\"input-group-append\">\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\n type=\"button\"></button>\n </div>\n </div>\n <input name=\"datepicker\" class=\"form-control visually-hidden\" ngbDatepicker #datepicker=\"ngbDatepicker\"\n [autoClose]=\"'outside'\" (dateSelect)=\"onDateSelection($event)\" [displayMonths]=\"2\" [dayTemplate]=\"t\"\n outsideDays=\"hidden\" [startDate]=\"fromDate || calendar.getToday()\" [container]=\"'body'\" tabindex=\"-1\">\n <ng-template #t let-date let-focused=\"focused\">\n <span class=\"custom-day\" [class.focused]=\"focused\" [class.range]=\"isRange(date)\"\n [class.faded]=\"isHovered(date) || isInside(date)\" (mouseenter)=\"hoveredDate = date\"\n (mouseleave)=\"hoveredDate = null\">\n {{ date.day }}\n </span>\n </ng-template>\n <span class=\"ms-1 p-2\">{{'to' | translate}}</span>\n <div class=\"input-group date-selector m-1\">\n <input #dpToDate class=\"form-control\" placeholder=\"dd/MM/yyyy\" name=\"dpToDate\"\n [value]=\"toDate | formatDateNumberToDigits\" (input)=\"toDate = validateInput(toDate, dpToDate.value)\">\n <div class=\"input-group-append\">\n <button class=\"btn btn-outline-secondary calendar\" (click)=\"datepicker.toggle()\"\n type=\"button\"></button>\n </div>\n </div>\n </div>\n <div class=\"input-group search-button-container d-flex flex-row flex-nowrap p-0 m-1 justify-content-evenly\">\n <button\n class=\"btn btn-primary rounded-pill ms-1 w-25 w-md-fit-content h-md-fit-content align-self-md-center\"\n type=\"button\" (click)=\"onApply()\" *ngIf=\"!isDateFilterApplied || dateChanged\">\n {{'Apply' | translate}}\n </button>\n <span\n class=\"appliedText ms-1 py-2 align-self-md-center pb-md-0 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\n *ngIf=\"isDateFilterApplied && !dateChanged\">\n <span class=\"iconify\" data-icon=\"ion:checkmark\" data-width=\"15\" data-height=\"15\"></span>\n {{\"Applied\" | translate}}\n </span>\n <button\n class=\"btn btn-outline-primary rounded-pill w-25 mx-2 w-md-fit-content h-md-fit-content align-self-md-center\"\n type=\"button\" (click)=\"onClearDates()\">{{'Clear'\n |\n translate}}</button>\n <div\n class=\"change-position-button align-self-end pb-2 align-self-md-center mx-2 pb-md-0 flex-nowrap w-25 w-md-fit-content h-md-fit-content\">\n <input type=\"checkbox\" class=\"btn-check\" id=\"btn-check-outlined\" autocomplete=\"off\">\n <label class=\"link-primary cursor-pointer text-decoration-underline\" (click)=\"onChangePositionClick()\"\n for=\"btn-check-outlined\">{{\n (isChangePositionVisible ? \"Close\" : \"Move to\") | translate }}</label>\n </div>\n </div>\n </form>\n </div>\n </div>\n </nav>\n <lib-navigation-bar *ngIf=\"isVisible && isChangePositionVisible\"></lib-navigation-bar>\n </div>\n <div *ngIf=\"isVisitor && isVisible\">\n <div class=\"visitor-div\">\n <div class=\"logo-container\" data-testid=\"dropdown-menu-button\">\n <img class=\"img-logo\" alt=\"SmarterPlanLogo\" src=\"../assets/images/logos/logo-reserve.png\">\n </div>\n <div class=\"avatar-container-menu\">\n <div class=\"profile_pic dropdown-toggle cursor-pointer\" data-bs-toggle=\"dropdown\">\n <lib-avatar></lib-avatar>\n </div>\n <div class=\"dropdown-menu dropdown-menu-right\">\n <button class=\"dropdown-item\" (click)=\"localeService.modal()\">{{'Language' | translate}}</button>\n <button class=\"dropdown-item\" (click)=\"openSupportForm()\">{{\"Help\" | translate}}</button>\n <button data-testid=\"logout-auth-btn\" class=\"dropdown-item\" (click)=\"signOut()\">{{\"Logout\" |\n translate}}</button>\n </div>\n </div>\n </div>\n </div>\n</div>", styles: ["#menu-bar{position:sticky;top:0;z-index:200}.navbar{z-index:200}.navbar-brand{padding-top:5px;padding-bottom:5px;cursor:pointer}.navbar-brand img{height:70px}.profile_pic{border-radius:50%;height:45px;width:45px;margin:8px;border:.2px solid rgb(150,150,150)}.navbar-nav .profile-pic-container .dropdown-menu{position:absolute}.navbar.second-nav{z-index:199;background-color:var(--smarterplan-primary)}.profile_pic.dropdown-toggle:after{display:none}.navbar-light{border-bottom:1px solid var(--smarterplan-primary);background-color:#fff}.navbar form{width:100%;margin:0;padding:0}input,select{margin-right:15px}.select{background:transparent;border:none;color:#fff;margin:6px 20px 0;width:auto}button{padding:5px 10px}button:disabled,button[disabled]{color:#c04f4f}.navbar-toggler{margin-right:1em}.icon{height:40px;width:40px;color:var(--smarterplan-primary)}.search-button-container{width:auto;align-items:stretch;justify-items:start}.search-button-container .search-button{width:auto;border-radius:1rem!important}.visitor-div{background-color:var(--smarterplan-primary);height:60px;display:flex;justify-content:space-between}.profile_pic-container{margin-right:1em}.logo-container,.img-logo{height:100%}.change-position-button{margin-left:2rem}.change-position-button label,.change-position-button .btn-check:checked,.change-position-button .btn-check:focus,.change-position-button .btn-outline-primary,.change-position-button .btn{box-shadow:none!important;outline:0}.change-position-button label{border-radius:16px}.change-position-button .btn-check:checked+.btn{background-color:var(--smarterplan-primary);color:#fff!important}.filters-menu-block{width:auto}@media (min-width: 1200px){.navbar-brand img{height:100px}.profile_pic{height:60px;width:60px;margin:15px}}@media (min-width: 992px) and (max-width: 1199.98px){.navbar-brand img{height:80px}.profile_pic{height:50px;width:50px;margin:12px}}@media (min-width: 768px) and (max-width: 991.98px){.navbar-brand img{height:60px}.profile_pic{height:40px;width:40px;margin:10px}}@media (max-width: 767.98px){.navbar-brand img{height:50px}.profile_pic{height:30px;width:30px;margin:8px}.search-button-container,.filters-menu-block{width:100%}}.form-container{margin:.5rem 0}.navbar-collapse.show{display:flex;padding-bottom:.5rem}.hidden{width:0;margin:0;border:none;padding:0}.custom-day{text-align:center;padding:.185rem .25rem;display:inline-block;height:2rem;width:2rem}.custom-day.focused{background-color:#e6e6e6}.custom-day.range,.custom-day:hover{background-color:var(--smarterplan-primary-lighter);color:#fff}.custom-day.faded{background-color:var(--smarterplan-primary-transparent)}.calendar{height:100%}.appliedText,.appliedText span{color:var(--smarterplan-primary)}@media (max-width: 767px){.form-inline{flex-direction:column}}.hidden{opacity:0}.date-selector{flex-wrap:nowrap}.date-selector input{min-width:80px;margin:0}@media (min-width: 768px){.w-md-fit-content{width:fit-content!important}.h-md-content{height:fit-content!important}}\n"] }]
13093
14746
  }], ctorParameters: () => [{ type: i1$1.Router }, { type: LocaleService }, { type: BaseUserService }, { type: NavigatorService }, { type: SupportService }, { type: BaseVisibilityService }, { type: i1$2.NgbCalendar }, { type: i1$2.NgbDateParserFormatter }, { type: FilterService }, { type: AmplifyCacheService }], propDecorators: { cacheName: [{
13094
14747
  type: Input
13095
14748
  }] } });
@@ -13244,27 +14897,27 @@ class BaseTagService {
13244
14897
  throw new Error('updatePoiMetadata is not implemented in the base class.');
13245
14898
  }*/
13246
14899
  async prepareEquipmentHtml(equip) {
13247
- throw new Error(`prepareEquipmentHtml is not implemented in the base class.
14900
+ throw new Error(`prepareEquipmentHtml is not implemented in the base class.
13248
14901
  Please override this method in your extended class.`);
13249
14902
  }
13250
14903
  async prepareTicketHtml(ticket) {
13251
- throw new Error(`prepareTicketHtml is not implemented in the base class.
14904
+ throw new Error(`prepareTicketHtml is not implemented in the base class.
13252
14905
  Please override this method in your extended class.`);
13253
14906
  }
13254
14907
  async prepareFeatureHtml(feature) {
13255
- throw new Error(`prepareFeatureHtml is not implemented in the base class.
14908
+ throw new Error(`prepareFeatureHtml is not implemented in the base class.
13256
14909
  Please override this method in your extended class.`);
13257
14910
  }
13258
14911
  async prepareMeasurementHtml(measure) {
13259
- throw new Error(`prepareMeasurementHtml is not implemented in the base class.
14912
+ throw new Error(`prepareMeasurementHtml is not implemented in the base class.
13260
14913
  Please override this method in your extended class.`);
13261
14914
  }
13262
14915
  async prepareDeskHtml(feature) {
13263
- throw new Error(`prepareDeskHtml is not implemented in the base class.
14916
+ throw new Error(`prepareDeskHtml is not implemented in the base class.
13264
14917
  Please override this method in your extended class.`);
13265
14918
  }
13266
14919
  async prepareIndicatorHtml(feature) {
13267
- throw new Error(`prepareDeskHtml is not implemented in the base class.
14920
+ throw new Error(`prepareDeskHtml is not implemented in the base class.
13268
14921
  Please override this method in your extended class.`);
13269
14922
  }
13270
14923
  async getSignedTagIconSource(tagIconSrc) {
@@ -13273,15 +14926,15 @@ class BaseTagService {
13273
14926
  getIconTagImageForFeature(feature, poi) {
13274
14927
  // const tagIcon = JSON.parse(poi.tagIcon);
13275
14928
  // return tagIcon.src;
13276
- throw new Error(`getIconTagImageForFeature is not implemented in the base class.
14929
+ throw new Error(`getIconTagImageForFeature is not implemented in the base class.
13277
14930
  Please override this method in your extended class.`);
13278
14931
  }
13279
14932
  getScriptForTag(object, tagType) {
13280
- throw new Error(`getScriptForTag is not implemented in the base class.
14933
+ throw new Error(`getScriptForTag is not implemented in the base class.
13281
14934
  Please override this method in your extended class.`);
13282
14935
  }
13283
14936
  getAnnexeForCommentTypeInFeature(feature, commentType) {
13284
- throw new Error(`getAnnexeForCommentTypeInFeature is not implemented in the base class.
14937
+ throw new Error(`getAnnexeForCommentTypeInFeature is not implemented in the base class.
13285
14938
  Please override this method in your extended class.`);
13286
14939
  }
13287
14940
  getBillboardMediaToEmbed(object) {
@@ -13290,27 +14943,27 @@ class BaseTagService {
13290
14943
  return { comment: null, tagDescription: '' };
13291
14944
  }
13292
14945
  onActionDetailClick(url) {
13293
- throw new Error(`onActionDetailClick is not implemented in the base class.
14946
+ throw new Error(`onActionDetailClick is not implemented in the base class.
13294
14947
  Please override this method in your extended class.`);
13295
14948
  }
13296
14949
  onActionAudioClick(audioCommentID) {
13297
- throw new Error(`onActionDetailClick is not implemented in the base class.
14950
+ throw new Error(`onActionDetailClick is not implemented in the base class.
13298
14951
  Please override this method in your extended class.`);
13299
14952
  }
13300
14953
  onActionVideoClick(url) {
13301
- throw new Error(`onActionVideoClick is not implemented in the base class.
14954
+ throw new Error(`onActionVideoClick is not implemented in the base class.
13302
14955
  Please override this method in your extended class.`);
13303
14956
  }
13304
14957
  onActionImageClick(imageCommentID) {
13305
- throw new Error(`onActionImageClick is not implemented in the base class.
14958
+ throw new Error(`onActionImageClick is not implemented in the base class.
13306
14959
  Please override this method in your extended class.`);
13307
14960
  }
13308
14961
  onActionDocClick(url) {
13309
- throw new Error(`onActionDocClick is not implemented in the base class.
14962
+ throw new Error(`onActionDocClick is not implemented in the base class.
13310
14963
  Please override this method in your extended class.`);
13311
14964
  }
13312
14965
  onActionYoutubeClick(url) {
13313
- throw new Error(`onActionYoutubeClick is not implemented in the base class.
14966
+ throw new Error(`onActionYoutubeClick is not implemented in the base class.
13314
14967
  Please override this method in your extended class.`);
13315
14968
  }
13316
14969
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BaseTagService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
@@ -13833,6 +15486,147 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
13833
15486
  args: [MATTERPORT_SDK_KEY]
13834
15487
  }] }, { type: NavigationService }, { type: ZoneService }, { type: ViewerService }, { type: LayerService }, { type: BaseUserService }, { type: PlanService }] });
13835
15488
 
15489
+ /** File extensions the Three.js viewer can load as a base model. */
15490
+ const SUPPORTED_MODEL_EXTENSIONS = ['.glb', '.gltf'];
15491
+ class ThreeJsImportService {
15492
+ navigationService;
15493
+ zoneService;
15494
+ layerService;
15495
+ userService;
15496
+ planService;
15497
+ _spaceID;
15498
+ get spaceID() {
15499
+ return this._spaceID;
15500
+ }
15501
+ set spaceID(value) {
15502
+ this._spaceID = value;
15503
+ }
15504
+ constructor(navigationService, zoneService, layerService, userService, planService) {
15505
+ this.navigationService = navigationService;
15506
+ this.zoneService = zoneService;
15507
+ this.layerService = layerService;
15508
+ this.userService = userService;
15509
+ this.planService = planService;
15510
+ }
15511
+ /**
15512
+ * Checks that an S3 key points at something the viewer can actually load.
15513
+ *
15514
+ * Returns the reason it can't, or null when it's fine. This runs before anything is written, so a
15515
+ * mistyped key fails on the form rather than creating zones and navigation for a model that will
15516
+ * 404 the first time someone opens the visit.
15517
+ */
15518
+ validateModelKey(key) {
15519
+ if (!key || !key.trim()) {
15520
+ return 'A 3D model file (.glb) is required for a Three.js visit.';
15521
+ }
15522
+ const trimmed = key.trim();
15523
+ if (!SUPPORTED_MODEL_EXTENSIONS.some((extension) => trimmed.toLowerCase().endsWith(extension))) {
15524
+ return `The 3D model must be a ${SUPPORTED_MODEL_EXTENSIONS.join(' or ')} file.`;
15525
+ }
15526
+ // A full https:// URL is a signed link that expires; the stored value has to be the key itself
15527
+ // so a fresh URL can be signed on every visit.
15528
+ if (/^https?:\/\//i.test(trimmed)) {
15529
+ return 'Enter the S3 key of the file (e.g. visits/my-model.glb), not a full URL.';
15530
+ }
15531
+ return null;
15532
+ }
15533
+ async importData(spaceID, surface, reloading = false, node = null, overrideExisting = true, placementMetadata = null) {
15534
+ if (reloading) {
15535
+ // Reloading is not fully applicable to static 3D models the same way as Matterport
15536
+ return Promise.resolve(true);
15537
+ }
15538
+ const keyError = this.validateModelKey(node?.model3D);
15539
+ if (keyError) {
15540
+ return Promise.reject(new Error(keyError));
15541
+ }
15542
+ this.spaceID = spaceID;
15543
+ const zones = await this.zoneService.getZonesBySpace(spaceID);
15544
+ const mainZone = zones.find((z) => !z.parentID);
15545
+ if (!mainZone) {
15546
+ return Promise.reject(new Error('Missing data (no main Building Zone)'));
15547
+ }
15548
+ console.log('[ThreeJsImportService] Importing data for Three.js model');
15549
+ // 1. Check/create layer of building type
15550
+ const layerBuilding = await this.getLayerByName('BUILDING');
15551
+ // We generate a dummy ID to act as our single "sweep" equivalent
15552
+ const dummySweepID = v4();
15553
+ // 2. Create the default Zone
15554
+ const createdDefaultZone = await this.createZone(node.metadata || 'Three.js 3D Model', layerBuilding.id, surface, mainZone.id, [dummySweepID], node);
15555
+ if (!createdDefaultZone) {
15556
+ return Promise.resolve(false);
15557
+ }
15558
+ // 3. Create a blank Plan for the model (to ensure consistency in the app structure)
15559
+ const plan = {
15560
+ name: `plan_threejs_${node.id}`,
15561
+ annexe: '', // No floorplan image generated automatically yet
15562
+ zoneID: createdDefaultZone.id,
15563
+ spaceID: this.spaceID,
15564
+ isCurrentForZone: true,
15565
+ isModified: false,
15566
+ isImportedMatterport: false, // Explicitly not matterport
15567
+ };
15568
+ await this.planService.createPlan(plan);
15569
+ // 4. Create the single Navigation entry this model is viewed from.
15570
+ // Matterport derives these from its sweeps; a static model has no sweeps, so the placement
15571
+ // comes from the metadata entered on the form, falling back to the origin.
15572
+ const placement = parseThreeJsPlacement(placementMetadata);
15573
+ const origin = { x: 0, y: 0, z: 0 };
15574
+ const navigationInput = {
15575
+ matterportSweepID: dummySweepID,
15576
+ position: JSON.stringify(placement?.cameraPosition ?? origin),
15577
+ rotation: JSON.stringify(placement?.rotation ?? origin),
15578
+ level: LevelStatus.LOW,
15579
+ nodeID: node.id,
15580
+ spaceID,
15581
+ };
15582
+ const nav = await this.navigationService.createNavigation(navigationInput);
15583
+ if (!nav) {
15584
+ console.log('Could not create navigation for', navigationInput);
15585
+ }
15586
+ console.log('[ThreeJsImportService] Import done.');
15587
+ return Promise.resolve(true);
15588
+ }
15589
+ async createZone(name, layerID, surface = null, parentID = null, sweeps = [], node = null, metadata = null) {
15590
+ const zoneInput = {
15591
+ name,
15592
+ spaceID: this.spaceID,
15593
+ layerID,
15594
+ sweepIDs: sweeps,
15595
+ nodeIDs: node ? [node.id] : null,
15596
+ };
15597
+ removeNullKeysFromObject(zoneInput);
15598
+ if (surface) {
15599
+ zoneInput.surface = surface;
15600
+ }
15601
+ if (parentID) {
15602
+ zoneInput.parentID = parentID;
15603
+ }
15604
+ if (metadata) {
15605
+ zoneInput.metadata = metadata;
15606
+ }
15607
+ const newLocal = await this.zoneService.create(zoneInput);
15608
+ return newLocal;
15609
+ }
15610
+ async getLayerByName(name) {
15611
+ const mission = this.userService.currentMission(this.spaceID);
15612
+ const currentOrgId = mission.organisationID;
15613
+ const layers = await this.layerService.getLayerByNameForOrganisation(name, currentOrgId);
15614
+ if (layers.length > 0) {
15615
+ return layers[0];
15616
+ }
15617
+ // Create it if it doesn't exist
15618
+ return this.layerService.createLayerForOrganisation(name, currentOrgId);
15619
+ }
15620
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ThreeJsImportService, deps: [{ token: NavigationService }, { token: ZoneService }, { token: LayerService }, { token: BaseUserService }, { token: PlanService }], target: i0.ɵɵFactoryTarget.Injectable });
15621
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ThreeJsImportService, providedIn: 'root' });
15622
+ }
15623
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ThreeJsImportService, decorators: [{
15624
+ type: Injectable,
15625
+ args: [{
15626
+ providedIn: 'root',
15627
+ }]
15628
+ }], ctorParameters: () => [{ type: NavigationService }, { type: ZoneService }, { type: LayerService }, { type: BaseUserService }, { type: PlanService }] });
15629
+
13836
15630
  // matterport-transform.util.ts
13837
15631
  /**
13838
15632
  * Computes the rigid transform (Y-axis rotation + translation) between two
@@ -16363,5 +18157,5 @@ function floatValidator() {
16363
18157
  * Generated bundle index. Do not edit.
16364
18158
  */
16365
18159
 
16366
- export { AffectationService, AvatarComponent, BaseLateralTabService, BaseTagService, BaseUserService, BaseVisibilityService, CameraMode, CaptureService, CaptureViewer, CommentService, CommentType, Config, ContentService, CsvExportComponent, DomainService, DomainType, DurationToStringPipe, EmailStatus, EquipmentService, EventService, EventStatus, EventType, FeatureService, FeatureType, FilterService, HashtagFromIdPipe, HashtagService, InterventionService, InventoryStatus, LayerService, LevelStatus, LoaderComponent, Locale, LocaleService, MATTERPORT_SDK_KEY, MatterportImportService, MatterportService, MatterportTagCopyService, MatterportViewType, MattertagActionMode, MattertagData, MeasurementService, MenuBarComponent, MissionService, NavigationService, NavigatorService, NgxSmarterplanCoreModule, NgxSmarterplanCoreService, NodeService, Object3DService, OperationService, OrganisationService, PaymentStatus, PlanService, PoiService, PoiType, ProfileEntity, ProfileService, ProfileStatus, PropertyService, PropertyType, RoleStatus, SafeUrlPipe, SearchBarComponent, SearchObjectType, SearchService, SpModule, SpaceService, SpaceStatus, StatusEquipment, SupportModalComponent, SupportService, TagAction, TemplateService, TicketPriority, TicketStatus, TicketType, TicketsService, TimeDateToLocalStringPipe, TypeNote, UsernameFromIdPipe, ValidatorsService, ViewerInteractions, ViewerService, VisitService, ZoneChangeService, ZoneService, applyRigidTransformToDirection, applyRigidTransformToPoint, applyRigidTransformToYRotationDeg, arraysContainSameElements, checkElementById, computeRigidTransform, convertTimestampToLocalZone, copyFileInS3, copyFileInS3ViaDownload, dateHasExpired, dateTimeToLocalString, deleteFromS3, distance, downloadBlob, downloadEquipmentDocument, downloadFile, downloadFileAsObject, durationToString, emailValidator, enumToArray, filterUniqueArrayByID, floatValidator, getBufferForFileFromS3, getCoefficientsForImage, getCurrentLang, getDistanceBetweenTwoPoints, getHighestLevelForMissions, getHighestRoleForMissions, getLevelsBelow, getLocaleLong, getLocaleShort, getMetaForImage, getRolesBelowForManager, getSignedFile, getSignedImageUrlForEquipment, getSignedImageUrlForProfile, getSignedImageUrlForSpace, getSpaceIDFromUrl, getVisitUrl, isEmptyObject, listFilesInFolder, mean, noEmptyValidator, numberToDateString, openDocument, openModalForVisitSwitch, poiTypeToString, pointToString, removeAllFilesFromFolderS3, removeNullKeysFromObject, showScanPointsOnPlanInDiv, shuffleArray, sortAlphabeticallyOnName, stringLocaleToEnum, stringToLowercaseNoSpaces, styleButton, textValidator, translateDatePeriod, uploadBase64Image, uploadBase64ImageWithRetry, uploadFileToS3, uploadJsonToS3, validEmail, wait, waitUntil };
18160
+ export { AffectationService, AvatarComponent, BaseLateralTabService, BaseTagService, BaseUserService, BaseVisibilityService, CameraMode, CaptureService, CaptureViewer, CommentService, CommentType, Config, ContentService, CsvExportComponent, DomainService, DomainType, DurationToStringPipe, EmailStatus, EquipmentService, EventService, EventStatus, EventType, FeatureService, FeatureType, FilterService, HashtagFromIdPipe, HashtagService, InterventionService, InventoryStatus, LayerService, LevelStatus, LoaderComponent, Locale, LocaleService, MATTERPORT_SDK_KEY, MatterportImportService, MatterportService, MatterportTagCopyService, MatterportViewType, MattertagActionMode, MattertagData, MeasurementService, MenuBarComponent, MissionService, NavigationService, NavigatorService, NgxSmarterplanCoreModule, NgxSmarterplanCoreService, NodeService, Object3DService, OperationService, OrganisationService, PaymentStatus, PlanService, PoiService, PoiType, ProfileEntity, ProfileService, ProfileStatus, PropertyService, PropertyType, RoleStatus, SafeUrlPipe, SearchBarComponent, SearchObjectType, SearchService, SpModule, SpaceService, SpaceStatus, StatusEquipment, SupportModalComponent, SupportService, TagAction, TemplateService, ThreeJsImportService, ThreeJsService, TicketPriority, TicketStatus, TicketType, TicketsService, TimeDateToLocalStringPipe, TypeNote, UsernameFromIdPipe, ValidatorsService, ViewerInteractions, ViewerService, VisitService, ZoneChangeService, ZoneService, applyRigidTransformToDirection, applyRigidTransformToPoint, applyRigidTransformToYRotationDeg, arraysContainSameElements, checkElementById, computeRigidTransform, convertTimestampToLocalZone, copyFileInS3, copyFileInS3ViaDownload, dateHasExpired, dateTimeToLocalString, deleteFromS3, distance, downloadBlob, downloadEquipmentDocument, downloadFile, downloadFileAsObject, durationToString, emailValidator, enumToArray, filterUniqueArrayByID, floatValidator, getBufferForFileFromS3, getCoefficientsForImage, getCurrentLang, getDistanceBetweenTwoPoints, getHighestLevelForMissions, getHighestRoleForMissions, getLevelsBelow, getLocaleLong, getLocaleShort, getMetaForImage, getRolesBelowForManager, getSignedFile, getSignedImageUrlForEquipment, getSignedImageUrlForProfile, getSignedImageUrlForSpace, getSpaceIDFromUrl, getVisitUrl, isEmptyObject, listFilesInFolder, mean, noEmptyValidator, numberToDateString, openDocument, openModalForVisitSwitch, parseThreeJsPlacement, poiTypeToString, pointToString, removeAllFilesFromFolderS3, removeNullKeysFromObject, showScanPointsOnPlanInDiv, shuffleArray, solveThreeJsAlignment, sortAlphabeticallyOnName, stringLocaleToEnum, stringToLowercaseNoSpaces, styleButton, textValidator, translateDatePeriod, uploadBase64Image, uploadBase64ImageWithRetry, uploadFileToS3, uploadFileToS3WithProgress, uploadJsonToS3, validEmail, wait, waitUntil };
16367
18161
  //# sourceMappingURL=smarterplan-ngx-smarterplan-core.mjs.map