@smarterplan/ngx-smarterplan-core 1.4.19 → 1.4.20

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 (101) 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 +28 -2
  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 +8 -3
  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 +10 -9
  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 +27 -2
  50. package/esm2022/lib/services/models/event.service.mjs +1 -1
  51. package/esm2022/lib/services/models/feature.service.mjs +14 -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 +17 -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 +1 -1
  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 +1 -1
  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 +1 -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/sweep-locator.service.mjs +114 -0
  75. package/esm2022/lib/services/tag.service.mjs +16 -16
  76. package/esm2022/lib/services/threejs-import.service.mjs +1 -1
  77. package/esm2022/lib/services/threejs.service.mjs +840 -35
  78. package/esm2022/lib/services/user.service.mjs +1 -1
  79. package/esm2022/lib/services/utils/matterport-transform.util.mjs +1 -1
  80. package/esm2022/lib/services/validators.service.mjs +1 -1
  81. package/esm2022/lib/services/viewer.service.mjs +193 -10
  82. package/esm2022/lib/services/zone-drawer.service.mjs +1 -1
  83. package/esm2022/lib/services/zoneChange.service.mjs +1 -1
  84. package/esm2022/lib/tokens/matterport-sdk-key.token.mjs +1 -1
  85. package/esm2022/lib/types.service.mjs +1 -1
  86. package/esm2022/lib/validators/email.directive.mjs +1 -1
  87. package/esm2022/lib/validators/no-empty.directive.mjs +1 -1
  88. package/esm2022/lib/validators/number.directive.mjs +1 -1
  89. package/esm2022/lib/validators/text.directive.mjs +1 -1
  90. package/esm2022/public-api.mjs +2 -1
  91. package/fesm2022/smarterplan-ngx-smarterplan-core.mjs +1771 -590
  92. package/fesm2022/smarterplan-ngx-smarterplan-core.mjs.map +1 -1
  93. package/lib/helpers.service.d.ts +17 -0
  94. package/lib/services/models/base-object.service.d.ts +3 -4
  95. package/lib/services/models/measurement.service.d.ts +1 -0
  96. package/lib/services/sweep-locator.service.d.ts +54 -0
  97. package/lib/services/threejs.service.d.ts +304 -10
  98. package/lib/services/viewer.service.d.ts +87 -2
  99. package/package.json +1 -1
  100. package/public-api.d.ts +1 -0
  101. package/src/assets/vs-app.json +67 -67
@@ -20,6 +20,9 @@ import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';
20
20
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
21
21
  import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader';
22
22
  import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
23
+ import { Line2 } from 'three/examples/jsm/lines/Line2';
24
+ import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry';
25
+ import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial';
23
26
  import * as FileSaver from 'file-saver';
24
27
  import levenshtein from 'js-levenshtein';
25
28
  import { GlobalWorkerOptions, getDocument } from 'pdfjs-dist';
@@ -87,11 +90,11 @@ class CsvExportComponent {
87
90
  link.remove();
88
91
  }
89
92
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CsvExportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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\">\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" }] });
93
+ 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" }] });
91
94
  }
92
95
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CsvExportComponent, decorators: [{
93
96
  type: Component,
94
- 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"] }]
97
+ 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"] }]
95
98
  }], propDecorators: { headers: [{
96
99
  type: Input
97
100
  }], rowsInput: [{
@@ -795,30 +798,30 @@ class SecurityCamera extends SceneComponent {
795
798
  }
796
799
  }
797
800
  function vertexShader$1() {
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
- }
801
+ return `
802
+ uniform mat4 lightMatrix;
803
+
804
+ varying vec4 vLightCoords;
805
+
806
+ void main() {
807
+ // create a UV region [0,1] x [0,1] to represent what the lightMatrix "sees"
808
+ vLightCoords = lightMatrix * modelMatrix * vec4(position, 1.0);
809
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
810
+ }
808
811
  `;
809
812
  }
810
813
  function fragmentShader$1() {
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
- }
814
+ return `
815
+ uniform vec3 color;
816
+
817
+ varying vec4 vLightCoords;
818
+
819
+ void main() {
820
+ vec2 lightUV = vLightCoords.xy / vLightCoords.w;
821
+ float inView = float(max(lightUV.x, lightUV.y) <= 1.0 && min(lightUV.x, lightUV.y) >= 0.0 && vLightCoords.z > 0.0);
822
+
823
+ gl_FragColor = vec4(color, min(inView, 0.2));
824
+ }
822
825
  `;
823
826
  }
824
827
  function updateHighlightUniforms$1(projector, uniforms) {
@@ -1011,30 +1014,30 @@ class ViewFrustumMesh extends SceneComponent {
1011
1014
  }
1012
1015
  }
1013
1016
  function vertexShader() {
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
- }
1017
+ return `
1018
+ uniform mat4 lightMatrix;
1019
+
1020
+ varying vec4 vLightCoords;
1021
+
1022
+ void main() {
1023
+ // create a UV region [0,1] x [0,1] to represent what the lightMatrix "sees"
1024
+ vLightCoords = lightMatrix * modelMatrix * vec4(position, 1.0);
1025
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
1026
+ }
1024
1027
  `;
1025
1028
  }
1026
1029
  function fragmentShader() {
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
- }
1030
+ return `
1031
+ uniform vec3 color;
1032
+
1033
+ varying vec4 vLightCoords;
1034
+
1035
+ void main() {
1036
+ vec2 lightUV = vLightCoords.xy / vLightCoords.w;
1037
+ float inView = float(max(lightUV.x, lightUV.y) <= 1.0 && min(lightUV.x, lightUV.y) >= 0.0 && vLightCoords.z > 0.0);
1038
+
1039
+ gl_FragColor = vec4(color, min(inView, 0.2));
1040
+ }
1038
1041
  `;
1039
1042
  }
1040
1043
  function updateHighlightUniforms(projector, uniforms) {
@@ -2197,32 +2200,32 @@ class PoiService {
2197
2200
  if (!sweeps || sweeps.length === 0) {
2198
2201
  return [];
2199
2202
  }
2200
- const queryStatement = `
2201
- query ListPOISLight($filter: ModelPOIFilterInput, $limit: Int, $nextToken: String) {
2202
- listPOIS(filter: $filter, limit: $limit, nextToken: $nextToken) {
2203
- items {
2204
- id
2205
- coordinate
2206
- specific
2207
- private
2208
- global
2209
- type
2210
- elementID
2211
- level
2212
- role
2213
- category
2214
- domainID
2215
- metadata
2216
- tagIcon
2217
- tagDescription
2218
- tagImageID
2219
- matterportSweepID
2220
- createdAt
2221
- updatedAt
2222
- }
2223
- nextToken
2224
- }
2225
- }
2203
+ const queryStatement = `
2204
+ query ListPOISLight($filter: ModelPOIFilterInput, $limit: Int, $nextToken: String) {
2205
+ listPOIS(filter: $filter, limit: $limit, nextToken: $nextToken) {
2206
+ items {
2207
+ id
2208
+ coordinate
2209
+ specific
2210
+ private
2211
+ global
2212
+ type
2213
+ elementID
2214
+ level
2215
+ role
2216
+ category
2217
+ domainID
2218
+ metadata
2219
+ tagIcon
2220
+ tagDescription
2221
+ tagImageID
2222
+ matterportSweepID
2223
+ createdAt
2224
+ updatedAt
2225
+ }
2226
+ nextToken
2227
+ }
2228
+ }
2226
2229
  `;
2227
2230
  const CHUNK_SIZE = 50; // safe margin for an "or" filter expression's practical size
2228
2231
  const chunks = [];
@@ -3027,11 +3030,11 @@ class MatterportPointerService {
3027
3030
  }
3028
3031
  updateCursorDisplay(poseMatterport) {
3029
3032
  if (this.textDisplayCursorPositionPanel && poseMatterport) {
3030
- this.textDisplayCursorPositionPanel.innerHTML = `position:
3031
- ${pointToString(poseMatterport.position)}\n
3032
- normal:
3033
- ${pointToString(poseMatterport.normal)}\n
3034
- floorId:
3033
+ this.textDisplayCursorPositionPanel.innerHTML = `position:
3034
+ ${pointToString(poseMatterport.position)}\n
3035
+ normal:
3036
+ ${pointToString(poseMatterport.normal)}\n
3037
+ floorId:
3035
3038
  ${poseMatterport.floorId}`;
3036
3039
  if (this.getCursorPositionButton) {
3037
3040
  this.getCursorPositionButton.style.display = 'none';
@@ -5181,22 +5184,22 @@ class MatterportService {
5181
5184
  );
5182
5185
  }*/
5183
5186
  addCustomCss() {
5184
- const css = `
5185
- .footer-ui,
5186
- .showcase-footer,
5187
- .mp-nova-btn-group,
5188
- .footer-divider,
5189
- .logo-link,
5190
- .footer-logo,
5191
- .bottom-ui,
5192
- img.logo-image,
5193
- img[class^="logo-image"],
5194
- img[class*=" logo-image"],
5195
- [data-testid="showcase-footer"] {
5196
- opacity: 0 !important;
5197
- visibility: hidden !important;
5198
- pointer-events: none !important;
5199
- }
5187
+ const css = `
5188
+ .footer-ui,
5189
+ .showcase-footer,
5190
+ .mp-nova-btn-group,
5191
+ .footer-divider,
5192
+ .logo-link,
5193
+ .footer-logo,
5194
+ .bottom-ui,
5195
+ img.logo-image,
5196
+ img[class^="logo-image"],
5197
+ img[class*=" logo-image"],
5198
+ [data-testid="showcase-footer"] {
5199
+ opacity: 0 !important;
5200
+ visibility: hidden !important;
5201
+ pointer-events: none !important;
5202
+ }
5200
5203
  `;
5201
5204
  // 1st method: Via Scene SDK
5202
5205
  if (this.sdk?.Scene?.addStyle) {
@@ -5794,20 +5797,29 @@ function frameFromPoints(a, b, c) {
5794
5797
  return new THREE.Matrix4().makeBasis(e1, e2, e1.clone().cross(e2));
5795
5798
  }
5796
5799
  /**
5797
- * Solves the transform that maps stored tag coordinates onto the loaded model.
5800
+ * Solves the transform that maps stored tag coordinates onto the loaded model, from as many matched
5801
+ * tags as there are - up to three.
5802
+ *
5803
+ * Three pairs pin down everything: position, orientation and scale. A model that shares only a
5804
+ * corner with the tags' space may not have three tags to offer, so fewer are accepted, each
5805
+ * settling what it can and assuming the rest:
5798
5806
  *
5799
- * Takes exactly the correspondences a person can produce by hand: three tags, each pointed at twice
5800
- * - once where it currently sits, once where it belongs. That is the minimum that pins down all
5801
- * three parts of the answer. One pair fixes only translation; two leave the rotation about the line
5802
- * between them free; three resolve it.
5807
+ * - Two pairs: position, heading and scale. The one thing two points cannot say is how the model
5808
+ * is tilted, so both spaces are taken to agree on which way is up.
5809
+ * - One pair: position only. Heading and scale are taken to match already.
5803
5810
  *
5804
- * Returns null when the points are collinear or degenerate, because a wrong alignment applied
5805
- * silently is worse than none.
5811
+ * `targetUp` is the model's vertical axis, which the tags' own vertical (+Y) is matched to in those
5812
+ * two cases. Returns null when the points cannot answer the question asked of them - collinear for
5813
+ * three, one above the other for two - because a wrong calibration applied silently is worse than
5814
+ * none.
5806
5815
  */
5807
- function solveThreeJsAlignment(pairs) {
5808
- if (!pairs || pairs.length < 3) {
5816
+ function solveThreeJsAlignment(pairs, targetUp = { x: 0, y: 1, z: 0 }) {
5817
+ if (!pairs || pairs.length === 0) {
5809
5818
  return null;
5810
5819
  }
5820
+ if (pairs.length < 3) {
5821
+ return solveReducedAlignment(pairs, new THREE.Vector3(targetUp.x, targetUp.y, targetUp.z).normalize());
5822
+ }
5811
5823
  const sources = pairs.slice(0, 3).map((pair) => new THREE.Vector3(pair.source.x, pair.source.y, pair.source.z));
5812
5824
  const targets = pairs.slice(0, 3).map((pair) => new THREE.Vector3(pair.target.x, pair.target.y, pair.target.z));
5813
5825
  const sourceFrame = frameFromPoints(sources[0], sources[1], sources[2]);
@@ -5848,6 +5860,41 @@ function solveThreeJsAlignment(pairs) {
5848
5860
  scale,
5849
5861
  };
5850
5862
  }
5863
+ /** The one- and two-pair cases of solveThreeJsAlignment, which share the "same vertical" assumption. */
5864
+ function solveReducedAlignment(pairs, up) {
5865
+ const sources = pairs.map((pair) => new THREE.Vector3(pair.source.x, pair.source.y, pair.source.z));
5866
+ const targets = pairs.map((pair) => new THREE.Vector3(pair.target.x, pair.target.y, pair.target.z));
5867
+ // Starts from the rotation that only stands the tags' vertical on the model's.
5868
+ const rotation = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), up);
5869
+ let scale = 1;
5870
+ if (pairs.length === 2) {
5871
+ const sourceSpan = sources[1].clone().sub(sources[0]);
5872
+ const targetSpan = targets[1].clone().sub(targets[0]);
5873
+ if (sourceSpan.length() < 1e-9 || targetSpan.length() < 1e-9) {
5874
+ return null;
5875
+ }
5876
+ scale = targetSpan.length() / sourceSpan.length();
5877
+ // Heading is the turn about the vertical that lines the two spans up, so only their horizontal
5878
+ // parts count. Two points one above the other have none, and say nothing about heading.
5879
+ const flatten = (v) => v.clone().sub(up.clone().multiplyScalar(v.dot(up)));
5880
+ const from = flatten(sourceSpan.clone().applyQuaternion(rotation));
5881
+ const to = flatten(targetSpan);
5882
+ if (from.length() < 1e-6 * sourceSpan.length() || to.length() < 1e-6 * targetSpan.length()) {
5883
+ return null;
5884
+ }
5885
+ const heading = Math.atan2(new THREE.Vector3().crossVectors(from, to).dot(up), from.dot(to));
5886
+ rotation.premultiply(new THREE.Quaternion().setFromAxisAngle(up, heading));
5887
+ }
5888
+ // A point lands at t + s*R*p; solved for t at the centroids, as in the three-pair case.
5889
+ const centroid = (points) => points.reduce((sum, v) => sum.add(v), new THREE.Vector3()).multiplyScalar(1 / points.length);
5890
+ const translation = centroid(targets).sub(centroid(sources).applyQuaternion(rotation).multiplyScalar(scale));
5891
+ const euler = new THREE.Euler().setFromQuaternion(rotation);
5892
+ return {
5893
+ translation: { x: translation.x, y: translation.y, z: translation.z },
5894
+ rotation: { x: euler.x, y: euler.y, z: euler.z },
5895
+ scale,
5896
+ };
5897
+ }
5851
5898
  class ThreeJsService {
5852
5899
  ngZone;
5853
5900
  scene;
@@ -5885,6 +5932,61 @@ class ThreeJsService {
5885
5932
  pendingTagPlacement = null;
5886
5933
  /** Pointer position waiting for a preview pick, coalesced to one per frame. */
5887
5934
  pendingPreviewEvent = null;
5935
+ /**
5936
+ * Tags whose position is in this model's own coordinates. A sibling of the tag group that does not
5937
+ * carry the calibration: calibrating a model for the tags it imports must not move the ones that
5938
+ * were placed straight onto it.
5939
+ */
5940
+ modelTagGroup = new THREE.Group();
5941
+ /**
5942
+ * The capture on screen. A tag placed in a 3D model is recorded with it, which is how the tags
5943
+ * placed in this one are told from those that came from elsewhere.
5944
+ */
5945
+ currentCaptureID = null;
5946
+ /**
5947
+ * The model's surface as a grid: every cell some part of the surface passes through.
5948
+ *
5949
+ * It answers "is this point near the model?" without visiting the triangles, of which a captured
5950
+ * model has millions - the question is asked for every tag, every time the calibration changes.
5951
+ */
5952
+ surfaceCells = null;
5953
+ surfaceOrigin = new THREE.Vector3();
5954
+ surfaceCellSize = 1;
5955
+ /**
5956
+ * How far from the model's surface a tag may be and still count as on it, in metres.
5957
+ *
5958
+ * Every tag of the space is offered to every model; this is what decides which ones a model
5959
+ * shows. A tag marks something on a surface, so one that belongs here sits on the surface or
5960
+ * close to it. One that lands deep inside the model, or well away from it, marks something that
5961
+ * is not part of this model. The allowance covers a calibration done by hand and the model not
5962
+ * matching the scan exactly, which together run to a few tens of centimetres.
5963
+ */
5964
+ static ON_MODEL_METRES = 0.75;
5965
+ /** Cells per axis the grid's keys can address, with room either side for points off the model. */
5966
+ static SURFACE_GRID_SPAN = 1024;
5967
+ static SURFACE_GRID_PAD = 128;
5968
+ /** Own tags moved to keep their place under the last calibration, waiting to be stored. */
5969
+ rebasedTags = [];
5970
+ /** The type filter in force. Null shows every type. */
5971
+ visibleTagTypes = null;
5972
+ measuring = false;
5973
+ /** Points clicked so far, in tag space. */
5974
+ measurePoints = [];
5975
+ /**
5976
+ * Everything drawn for the measurement. A child of the tag group, so its points share the frame
5977
+ * tag coordinates are stored in - which is metres, and what the saved POI has to be in.
5978
+ */
5979
+ measureGroup = new THREE.Group();
5980
+ /** The segment that follows the pointer from the last point clicked. */
5981
+ measureRubberBand;
5982
+ lastMeasurement = null;
5983
+ /** Half the model's diagonal as placed in the scene, where it is centred on the origin. */
5984
+ modelWorldRadius = 0;
5985
+ /** The outermost group the model and its tags hang from: where "which way is up" is applied. */
5986
+ modelGroup;
5987
+ modelUp = 'y';
5988
+ /** While measuring: how many points and each segment's length. Null when it is abandoned. */
5989
+ measurementProgress$ = new Subject();
5888
5990
  /**
5889
5991
  * Layer carrying the model alone, so the depth pick can render it without the tags.
5890
5992
  *
@@ -5892,6 +5994,8 @@ class ThreeJsService {
5892
5994
  * their parent passed, so marking the model's own meshes is enough - no re-parenting.
5893
5995
  */
5894
5996
  static PICK_LAYER = 1;
5997
+ /** The app's highlight colour, so a measurement reads as the active thing on screen. */
5998
+ static MEASURE_COLOR = '#FFB103';
5895
5999
  /** Cache Storage bucket holding downloaded models, and how many to keep. */
5896
6000
  static MODEL_CACHE = 'smarterplan-3d-models';
5897
6001
  static MODEL_CACHE_SIZE = 3;
@@ -5908,6 +6012,8 @@ class ThreeJsService {
5908
6012
  aligning = false;
5909
6013
  alignmentPairs = [];
5910
6014
  alignmentSource = null;
6015
+ /** The tag `alignmentSource` was read from, kept so the same tag is not matched twice. */
6016
+ alignmentSourceTagId;
5911
6017
  /** Drives the alignment walkthrough's UI: how many pairs are done and what it wants next. */
5912
6018
  alignmentProgress$ = new Subject();
5913
6019
  /** In-flight camera move started by goToTag, advanced by the render loop. */
@@ -6023,6 +6129,8 @@ class ThreeJsService {
6023
6129
  // new model and show the previous visit's tags on it.
6024
6130
  this.discardCurrentLoad();
6025
6131
  this.clearTags();
6132
+ this.measuring = false;
6133
+ this.clearMeasurement();
6026
6134
  this.cameraTween = null;
6027
6135
  this.modelRoot = undefined;
6028
6136
  // 1. Setup Scene
@@ -6130,7 +6238,7 @@ class ThreeJsService {
6130
6238
  * and stays indexed, so far less data reaches the GPU. KTX2 textures are supported too: they
6131
6239
  * stay compressed in VRAM instead of being decoded to raw pixels like JPEG/PNG.
6132
6240
  */
6133
- loadGLTFModel(url, cameraPosition, rotation, cacheKey) {
6241
+ loadGLTFModel(url, cameraPosition, rotation, cacheKey, up) {
6134
6242
  const loader = new GLTFLoader();
6135
6243
  // KTX2 needs the renderer to report which compressed formats this GPU accepts, so this has to
6136
6244
  // happen after init(). Guarded so a model without KTX2 textures still loads if it's missing.
@@ -6156,7 +6264,7 @@ class ThreeJsService {
6156
6264
  this.forceDoubleSidedMaterials(gltf.scene);
6157
6265
  this.ensureVertexNormals(gltf.scene);
6158
6266
  this.tuneTextureSampling(gltf.scene);
6159
- this.placeLoadedObjectInScene(gltf.scene, cameraPosition, rotation);
6267
+ this.placeLoadedObjectInScene(gltf.scene, cameraPosition, rotation, up);
6160
6268
  this.modelLoading$.next(false);
6161
6269
  };
6162
6270
  const onError = (error) => {
@@ -6629,7 +6737,7 @@ class ThreeJsService {
6629
6737
  });
6630
6738
  }
6631
6739
  /** Centers, scales and adds a loaded object to the scene, then frames the camera on it. */
6632
- placeLoadedObjectInScene(object, cameraPosition, rotation) {
6740
+ placeLoadedObjectInScene(object, cameraPosition, rotation, up) {
6633
6741
  const box = new THREE.Box3().setFromObject(object);
6634
6742
  const center = box.getCenter(new THREE.Vector3());
6635
6743
  const size = box.getSize(new THREE.Vector3());
@@ -6644,12 +6752,25 @@ class ThreeJsService {
6644
6752
  if (rotation) {
6645
6753
  object.rotation.set(rotation.x, rotation.y, rotation.z);
6646
6754
  }
6755
+ // Which way is up. A choice someone made by hand always wins; otherwise, unless the model came
6756
+ // with an orientation of its own, it is worked out from the geometry. Applied to the outer
6757
+ // group, above both the model and the tags: they turn together, and every coordinate already
6758
+ // stored - tag positions, the calibration - keeps meaning what it meant.
6759
+ this.modelGroup = group;
6760
+ this.modelUp = up ?? (rotation ? 'y' : this.detectModelUp(object, size));
6761
+ group.quaternion.copy(this.uprightRotation(this.modelUp));
6647
6762
  this.modelRoot = object;
6763
+ this.modelWorldRadius = (size.length() / 2) * scale;
6764
+ // The box above was measured before any rotation from the metadata was applied; with one, it
6765
+ // has to be measured again to be the model as it actually sits.
6766
+ this.buildSurfaceGrid(object, rotation ? new THREE.Box3().setFromObject(object) : box);
6767
+ this.refreshTagVisibility();
6648
6768
  // Marks the model for the depth pick, which renders this layer alone - so tags and the ghost
6649
6769
  // itself cannot end up being what the cursor reports as the surface under it.
6650
6770
  object.traverse((child) => child.layers.enable(ThreeJsService.PICK_LAYER));
6651
6771
  centered.add(object);
6652
6772
  centered.add(this.tagGroup);
6773
+ centered.add(this.modelTagGroup);
6653
6774
  group.add(centered);
6654
6775
  group.scale.setScalar(scale);
6655
6776
  this.scene.add(group);
@@ -6665,6 +6786,98 @@ class ThreeJsService {
6665
6786
  this.controls.target.set(0, 0, 0);
6666
6787
  this.controls.update();
6667
6788
  }
6789
+ /**
6790
+ * Which of the model's axes is its vertical: 'y' when it already stands upright, or when it is
6791
+ * not clear enough to act on.
6792
+ *
6793
+ * Models arrive in two conventions: Y up (glTF, and what this viewer assumes) and Z up (most CAD
6794
+ * and scanning exports). A Z-up model loads lying on its side. Two independent cues say which
6795
+ * axis is the vertical one, and both have to agree before anything is turned:
6796
+ *
6797
+ * - Surface area. Floors and ceilings all face along the vertical, while walls are shared out
6798
+ * between the two horizontal axes, so the vertical usually collects the most area.
6799
+ * - Extent. A building is normally wider than it is tall, so the vertical is its shortest side.
6800
+ *
6801
+ * Either alone is wrong often enough to matter - a long corridor has more wall than floor, a
6802
+ * tower is taller than it is wide - and turning a model that was fine is worse than leaving one
6803
+ * that was not. A model that already has Y as its vertical is never touched.
6804
+ */
6805
+ detectModelUp(object, size) {
6806
+ const axes = ['x', 'y', 'z'];
6807
+ // Projected area facing each way along each axis: [positive, negative].
6808
+ const area = { x: [0, 0], y: [0, 0], z: [0, 0] };
6809
+ let triangleCount = 0;
6810
+ object.updateMatrixWorld(true);
6811
+ object.traverse((child) => {
6812
+ const position = child.isMesh && child.geometry?.attributes?.position;
6813
+ if (position) {
6814
+ triangleCount += (child.geometry.index ? child.geometry.index.count : position.count) / 3;
6815
+ }
6816
+ });
6817
+ if (triangleCount === 0) {
6818
+ return 'y';
6819
+ }
6820
+ // A sample is plenty to see where the area lies, and keeps this cheap on models with millions
6821
+ // of triangles.
6822
+ const stride = Math.max(1, Math.ceil(triangleCount / 200000));
6823
+ const a = new THREE.Vector3();
6824
+ const b = new THREE.Vector3();
6825
+ const c = new THREE.Vector3();
6826
+ const normal = new THREE.Vector3();
6827
+ object.traverse((child) => {
6828
+ const position = child.isMesh && child.geometry?.attributes?.position;
6829
+ if (!position) {
6830
+ return;
6831
+ }
6832
+ const index = child.geometry.index;
6833
+ const count = (index ? index.count : position.count) / 3;
6834
+ for (let triangle = 0; triangle < count; triangle += stride) {
6835
+ const offset = triangle * 3;
6836
+ a.fromBufferAttribute(position, index ? index.getX(offset) : offset).applyMatrix4(child.matrixWorld);
6837
+ b.fromBufferAttribute(position, index ? index.getX(offset + 1) : offset + 1).applyMatrix4(child.matrixWorld);
6838
+ c.fromBufferAttribute(position, index ? index.getX(offset + 2) : offset + 2).applyMatrix4(child.matrixWorld);
6839
+ // The cross product's length is twice the triangle's area, so its components are the
6840
+ // area the triangle shows along each axis.
6841
+ normal.crossVectors(b.sub(a), c.sub(a));
6842
+ axes.forEach((axis) => {
6843
+ area[axis][normal[axis] >= 0 ? 0 : 1] += Math.abs(normal[axis]);
6844
+ });
6845
+ }
6846
+ });
6847
+ const total = { x: area.x[0] + area.x[1], y: area.y[0] + area.y[1], z: area.z[0] + area.z[1] };
6848
+ const byArea = axes.reduce((best, axis) => (total[axis] > total[best] ? axis : best), 'y');
6849
+ const byExtent = axes.reduce((best, axis) => (size[axis] < size[best] ? axis : best), 'y');
6850
+ if (byArea === 'y' || byArea !== byExtent || total[byArea] < 1.25 * total.y) {
6851
+ return 'y';
6852
+ }
6853
+ // Which way along that axis is up. Roofs and ground make an outdoor model face mostly upward,
6854
+ // which settles it; indoors floors and ceilings balance out, and the convention - positive is
6855
+ // up - is the better guess.
6856
+ const [positive, negative] = area[byArea];
6857
+ const up = `${negative > 0.65 * (positive + negative) ? '-' : ''}${byArea}`;
6858
+ console.info(`ThreeJsService: this model's vertical axis looks like ${up}, not y - standing it upright.`);
6859
+ return up;
6860
+ }
6861
+ /** The rotation that brings the given model axis to point up. */
6862
+ uprightRotation(up) {
6863
+ const axis = new THREE.Vector3();
6864
+ axis[up.replace('-', '')] = up.startsWith('-') ? -1 : 1;
6865
+ return new THREE.Quaternion().setFromUnitVectors(axis, new THREE.Vector3(0, 1, 0));
6866
+ }
6867
+ /** Which of the model's axes is currently treated as up, whether chosen or worked out. */
6868
+ getModelUp() {
6869
+ return this.modelUp;
6870
+ }
6871
+ /**
6872
+ * Turns the model so the given axis points up, at once.
6873
+ *
6874
+ * The model and its tags turn as one, so nothing stored has to be recomputed. The caller is
6875
+ * expected to store the choice, or the next load goes back to working it out.
6876
+ */
6877
+ setModelUp(up) {
6878
+ this.modelUp = up;
6879
+ this.modelGroup?.quaternion.copy(this.uprightRotation(up));
6880
+ }
6668
6881
  /**
6669
6882
  * Keeps the drawing buffer and camera matched to the canvas as it is resized.
6670
6883
  *
@@ -6690,6 +6903,7 @@ class ThreeJsService {
6690
6903
  this.camera.aspect = width / height;
6691
6904
  this.camera.updateProjectionMatrix();
6692
6905
  this.renderer.setSize(width, height);
6906
+ this.updateMeasureLineResolution();
6693
6907
  });
6694
6908
  this.canvasResizeObserver.observe(container);
6695
6909
  }
@@ -6701,9 +6915,16 @@ class ThreeJsService {
6701
6915
  * applied until solveAlignmentFromPairs() succeeds.
6702
6916
  */
6703
6917
  startTagAlignment() {
6918
+ // One pointer mode at a time, the same rule startMeasuring() applies the other way round.
6919
+ if (this.measuring) {
6920
+ this.cancelMeasuring();
6921
+ }
6704
6922
  this.aligning = true;
6705
6923
  this.alignmentPairs = [];
6706
6924
  this.alignmentSource = null;
6925
+ // While calibrating, the tags from elsewhere are all shown - including the ones off the model,
6926
+ // which are exactly the ones a calibration is for.
6927
+ this.refreshTagVisibility();
6707
6928
  if (this.renderer) {
6708
6929
  this.renderer.domElement.style.cursor = 'crosshair';
6709
6930
  }
@@ -6716,6 +6937,7 @@ class ThreeJsService {
6716
6937
  this.alignmentSource = null;
6717
6938
  this.pendingPreviewEvent = null;
6718
6939
  this.removePlacementPreview();
6940
+ this.refreshTagVisibility();
6719
6941
  if (this.renderer) {
6720
6942
  this.renderer.domElement.style.cursor = 'default';
6721
6943
  }
@@ -6735,12 +6957,22 @@ class ThreeJsService {
6735
6957
  * first half of a pair, which is a click on a tag, not on the model.
6736
6958
  */
6737
6959
  isPreviewingPlacement() {
6738
- return this.placingTag || (this.aligning && !!this.alignmentSource);
6960
+ return this.placingTag || this.measuring || (this.aligning && !!this.alignmentSource);
6739
6961
  }
6740
6962
  /** Whether a tag is currently registered for this element. */
6741
6963
  hasTag(elementId) {
6742
6964
  return this.tagSprites.has(elementId);
6743
6965
  }
6966
+ /** How many tags are on show - the ones a person could actually click. */
6967
+ getTagCount() {
6968
+ let visible = 0;
6969
+ this.tagSprites.forEach((sprite) => {
6970
+ if (sprite.visible) {
6971
+ visible += 1;
6972
+ }
6973
+ });
6974
+ return visible;
6975
+ }
6744
6976
  /**
6745
6977
  * Solves and applies the alignment collected so far, returning it so the caller can persist it.
6746
6978
  *
@@ -6748,35 +6980,105 @@ class ThreeJsService {
6748
6980
  * the walkthrough stays open for the person to pick a better third point.
6749
6981
  */
6750
6982
  solveAlignmentFromPairs() {
6751
- const alignment = solveThreeJsAlignment(this.alignmentPairs);
6983
+ const up = new THREE.Vector3();
6984
+ up[this.modelUp.replace('-', '')] = this.modelUp.startsWith('-') ? -1 : 1;
6985
+ const alignment = solveThreeJsAlignment(this.alignmentPairs, up);
6752
6986
  if (!alignment) {
6753
6987
  return null;
6754
6988
  }
6755
- this.setTagAlignment(alignment);
6989
+ // Out of calibrating mode first: applying the result re-decides which tags show, and from here
6990
+ // on that is the ones on the model again, not all of them.
6756
6991
  this.aligning = false;
6992
+ this.applyAlignmentKeepingOwnTags(alignment);
6757
6993
  this.pendingPreviewEvent = null;
6758
6994
  this.removePlacementPreview();
6759
6995
  this.renderer.domElement.style.cursor = 'default';
6760
6996
  this.alignmentProgress$.next({ pairs: this.alignmentPairs.length, awaiting: null });
6761
6997
  return alignment;
6762
6998
  }
6999
+ /**
7000
+ * Applies a new calibration while keeping every tag placed in this model exactly where it is.
7001
+ *
7002
+ * A calibration is worked out for the tags that came from elsewhere. The model's own tags were put
7003
+ * where they are by someone clicking the model, so they must not follow it. Their stored
7004
+ * coordinates are rewritten instead: into the shared space as the new calibration defines it.
7005
+ * That is also what makes them comparable with other visits from here on - a tag placed before
7006
+ * the model was calibrated was in the model's own coordinates, visible nowhere else.
7007
+ *
7008
+ * The rewritten tags are kept for the caller to store; see takeRebasedTags().
7009
+ */
7010
+ applyAlignmentKeepingOwnTags(alignment) {
7011
+ const own = [...this.tagSprites.values()].filter((sprite) => this.isOwnedByVisit(sprite));
7012
+ // Where each one is now, in the space the model sits in, and how its parent was turned.
7013
+ const placed = own.map((sprite) => {
7014
+ sprite.parent?.updateMatrix();
7015
+ return {
7016
+ sprite,
7017
+ position: sprite.position.clone().applyMatrix4(sprite.parent?.matrix ?? new THREE.Matrix4()),
7018
+ turn: sprite.parent?.quaternion.clone() ?? new THREE.Quaternion(),
7019
+ };
7020
+ });
7021
+ this.setTagAlignment(alignment);
7022
+ const inverse = this.tagGroup.matrix.clone().invert();
7023
+ const unturn = this.tagGroup.quaternion.clone().invert();
7024
+ this.rebasedTags = placed.map(({ sprite, position, turn }) => {
7025
+ sprite.position.copy(position.applyMatrix4(inverse));
7026
+ sprite.userData.frame = null;
7027
+ this.parentTag(sprite);
7028
+ this.scaleTagForModel(sprite);
7029
+ const rotation = unturn.clone().multiply(turn);
7030
+ return {
7031
+ id: sprite.name,
7032
+ position: { x: sprite.position.x, y: sprite.position.y, z: sprite.position.z },
7033
+ rotateDirection: ({ x, y, z }) => {
7034
+ const turned = new THREE.Vector3(x, y, z).applyQuaternion(rotation);
7035
+ return { x: turned.x, y: turned.y, z: turned.z };
7036
+ },
7037
+ };
7038
+ });
7039
+ this.refreshTagVisibility();
7040
+ }
7041
+ /** Hands over the tags the last calibration rewrote, once, for them to be stored. */
7042
+ takeRebasedTags() {
7043
+ const rebased = this.rebasedTags;
7044
+ this.rebasedTags = [];
7045
+ return rebased;
7046
+ }
7047
+ /**
7048
+ * Chooses the tag for the pair being matched by id, instead of by clicking it.
7049
+ *
7050
+ * Clicking a tag supposes it can be found. On a model that has never been calibrated the tags may
7051
+ * sit far from it, or all on top of one another; a list has neither problem.
7052
+ */
7053
+ selectAlignmentTag(elementId) {
7054
+ const sprite = this.tagSprites.get(elementId);
7055
+ if (!this.aligning || this.alignmentSource || !sprite || !this.calibrationCandidates().includes(sprite)) {
7056
+ return false;
7057
+ }
7058
+ this.setAlignmentSource(sprite);
7059
+ return true;
7060
+ }
7061
+ setAlignmentSource(sprite) {
7062
+ // The sprite's own position is the stored coordinate, untouched by any alignment already
7063
+ // applied - that transform lives on the group, not on the sprite.
7064
+ const stored = sprite.position;
7065
+ this.alignmentSource = { x: stored.x, y: stored.y, z: stored.z };
7066
+ this.alignmentSourceTagId = sprite.name;
7067
+ this.alignmentProgress$.next({
7068
+ pairs: this.alignmentPairs.length,
7069
+ awaiting: 'point',
7070
+ tagId: sprite.name,
7071
+ });
7072
+ }
6763
7073
  /** Handles a click while the alignment walkthrough is running. */
6764
7074
  handleAlignmentClick(event) {
6765
7075
  if (!this.alignmentSource) {
6766
7076
  // First half of a pair: which tag are we talking about?
6767
- const hit = this.raycaster.intersectObjects(this.tagGroup.children, false)[0];
7077
+ const hit = this.raycaster.intersectObjects(this.calibrationCandidates(), false)[0];
6768
7078
  if (!hit) {
6769
7079
  return;
6770
7080
  }
6771
- // The sprite's own position is the stored coordinate, untouched by any alignment already
6772
- // applied - that transform lives on the group, not on the sprite.
6773
- const stored = hit.object.position;
6774
- this.alignmentSource = { x: stored.x, y: stored.y, z: stored.z };
6775
- this.alignmentProgress$.next({
6776
- pairs: this.alignmentPairs.length,
6777
- awaiting: 'point',
6778
- tagId: hit.object.name,
6779
- });
7081
+ this.setAlignmentSource(hit.object);
6780
7082
  return;
6781
7083
  }
6782
7084
  // Second half: where that tag actually belongs on the model.
@@ -6784,15 +7086,249 @@ class ThreeJsService {
6784
7086
  if (!target) {
6785
7087
  return;
6786
7088
  }
6787
- this.alignmentPairs.push({ source: this.alignmentSource, target });
7089
+ this.alignmentPairs.push({ tagId: this.alignmentSourceTagId, source: this.alignmentSource, target });
6788
7090
  this.alignmentSource = null;
6789
7091
  // The ghost belongs to the click that just happened; the next half is a click on a tag.
6790
7092
  this.pendingPreviewEvent = null;
6791
7093
  this.removePlacementPreview();
6792
7094
  this.alignmentProgress$.next({
6793
7095
  pairs: this.alignmentPairs.length,
6794
- awaiting: this.alignmentPairs.length >= 3 ? null : 'tag',
7096
+ // Done at three, or sooner when there are no more tags to match: fewer pairs still calibrate,
7097
+ // only less completely.
7098
+ awaiting: this.alignmentPairs.length >= 3 || this.calibrationCandidates().length === 0 ? null : 'tag',
7099
+ });
7100
+ }
7101
+ /**
7102
+ * Starts a measurement: every click on the model adds a point, and each pair of consecutive
7103
+ * points is a measured segment.
7104
+ *
7105
+ * It ends through finishMeasuring() or cancelMeasuring() rather than on its own after two points,
7106
+ * because a measurement may have several segments - the same as Matterport's, whose results are
7107
+ * stored as a list of distances.
7108
+ */
7109
+ startMeasuring() {
7110
+ // One pointer mode at a time: a click cannot be both a tag position and a measured point.
7111
+ this.cancelTagPlacement();
7112
+ if (this.aligning) {
7113
+ this.cancelTagAlignment();
7114
+ }
7115
+ this.clearMeasurement();
7116
+ if (this.measureGroup.parent !== this.tagGroup) {
7117
+ this.tagGroup.add(this.measureGroup);
7118
+ }
7119
+ this.measuring = true;
7120
+ if (this.renderer) {
7121
+ this.renderer.domElement.style.cursor = 'crosshair';
7122
+ }
7123
+ this.measurementProgress$.next({ points: 0, distances: [] });
7124
+ }
7125
+ /**
7126
+ * Abandons the measurement and removes what was drawn.
7127
+ *
7128
+ * Also what clears a finished measurement off the model once its form is saved or closed, which
7129
+ * is why it only reports an abandonment when one was actually in progress.
7130
+ */
7131
+ cancelMeasuring() {
7132
+ const wasMeasuring = this.measuring;
7133
+ this.measuring = false;
7134
+ this.pendingPreviewEvent = null;
7135
+ this.removePlacementPreview();
7136
+ this.clearMeasurement();
7137
+ // Forgotten here so that a form opened later cannot pick up a measurement already dealt with.
7138
+ this.lastMeasurement = null;
7139
+ if (this.renderer) {
7140
+ this.renderer.domElement.style.cursor = 'default';
7141
+ }
7142
+ if (wasMeasuring) {
7143
+ this.measurementProgress$.next(null);
7144
+ }
7145
+ }
7146
+ /**
7147
+ * Ends the measurement and returns it, or null when there are not yet two points to measure
7148
+ * between - in which case measuring simply carries on.
7149
+ *
7150
+ * The drawing stays on the model so it is still visible while the person names the measurement;
7151
+ * cancelMeasuring() takes it away afterwards.
7152
+ */
7153
+ finishMeasuring() {
7154
+ if (!this.measuring || this.measurePoints.length < 2) {
7155
+ return null;
7156
+ }
7157
+ this.measuring = false;
7158
+ this.pendingPreviewEvent = null;
7159
+ this.removePlacementPreview();
7160
+ this.updateMeasureRubberBand(null);
7161
+ this.renderer.domElement.style.cursor = 'default';
7162
+ this.lastMeasurement = {
7163
+ points: this.measurePoints.map((point) => ({ x: point.x, y: point.y, z: point.z })),
7164
+ distances: this.measureDistances(),
7165
+ screenshot: this.captureMeasurementScreenshot(),
7166
+ };
7167
+ return this.lastMeasurement;
7168
+ }
7169
+ isMeasuring() {
7170
+ return this.measuring;
7171
+ }
7172
+ /** The measurement finishMeasuring() last produced, for the form that saves it. */
7173
+ getLastMeasurement() {
7174
+ return this.lastMeasurement;
7175
+ }
7176
+ /** Metres between consecutive points. Tag space is in metres, so no conversion is involved. */
7177
+ measureDistances() {
7178
+ const distances = [];
7179
+ for (let index = 1; index < this.measurePoints.length; index += 1) {
7180
+ const distance = this.measurePoints[index - 1].distanceTo(this.measurePoints[index]);
7181
+ // Two decimals, the precision Matterport's own measurements are stored with.
7182
+ distances.push(Math.round(distance * 100) / 100);
7183
+ }
7184
+ return distances;
7185
+ }
7186
+ addMeasurePoint(point) {
7187
+ const previous = this.measurePoints[this.measurePoints.length - 1];
7188
+ this.measurePoints.push(point);
7189
+ const marker = new THREE.Sprite(new THREE.SpriteMaterial({
7190
+ map: this.createTagTexture(ThreeJsService.MEASURE_COLOR),
7191
+ depthTest: false,
7192
+ transparent: true,
7193
+ // Sized against the viewport, not the model: measuring is done zoomed right in, where
7194
+ // anything sized in metres would cover the very thing being measured.
7195
+ sizeAttenuation: false,
7196
+ }));
7197
+ marker.scale.set(0.022, 0.022, 1);
7198
+ marker.position.copy(point);
7199
+ marker.renderOrder = 1001;
7200
+ this.measureGroup.add(marker);
7201
+ if (previous) {
7202
+ this.measureGroup.add(this.createMeasureLine(previous, point));
7203
+ const distance = Math.round(previous.distanceTo(point) * 100) / 100;
7204
+ const label = new THREE.Sprite(new THREE.SpriteMaterial({
7205
+ map: this.createMeasureLabelTexture(`${distance.toFixed(2)} m`),
7206
+ depthTest: false,
7207
+ transparent: true,
7208
+ sizeAttenuation: false,
7209
+ }));
7210
+ label.scale.set(0.15, 0.05, 1);
7211
+ label.position.copy(previous).add(point).multiplyScalar(0.5);
7212
+ label.renderOrder = 1002;
7213
+ this.measureGroup.add(label);
7214
+ }
7215
+ this.measurementProgress$.next({
7216
+ points: this.measurePoints.length,
7217
+ distances: this.measureDistances(),
7218
+ });
7219
+ }
7220
+ /**
7221
+ * A segment with a width in pixels.
7222
+ *
7223
+ * WebGL's own lines are one pixel wide whatever is asked for, which all but disappears over a
7224
+ * photographic texture. This kind is drawn as screen-facing geometry, so the width holds.
7225
+ */
7226
+ createMeasureLine(from, to) {
7227
+ const geometry = new LineGeometry();
7228
+ geometry.setPositions([from.x, from.y, from.z, to.x, to.y, to.z]);
7229
+ const material = new LineMaterial({
7230
+ color: new THREE.Color(ThreeJsService.MEASURE_COLOR).getHex(),
7231
+ linewidth: 3,
7232
+ depthTest: false,
7233
+ transparent: true,
7234
+ });
7235
+ const size = this.renderer.getSize(new THREE.Vector2());
7236
+ material.resolution.set(size.x, size.y);
7237
+ const line = new Line2(geometry, material);
7238
+ line.renderOrder = 1000;
7239
+ return line;
7240
+ }
7241
+ /** Line widths are in pixels, which the material can only honour if it knows the canvas size. */
7242
+ updateMeasureLineResolution() {
7243
+ if (!this.renderer) {
7244
+ return;
7245
+ }
7246
+ const size = this.renderer.getSize(new THREE.Vector2());
7247
+ this.measureGroup.children.forEach((child) => {
7248
+ if (child.material?.resolution) {
7249
+ child.material.resolution.set(size.x, size.y);
7250
+ }
7251
+ });
7252
+ }
7253
+ /** Stretches a segment from the last point to the pointer, or hides it when given null. */
7254
+ updateMeasureRubberBand(target) {
7255
+ const last = this.measurePoints[this.measurePoints.length - 1];
7256
+ if (!this.measuring || !last || !target) {
7257
+ if (this.measureRubberBand) {
7258
+ this.measureRubberBand.visible = false;
7259
+ }
7260
+ return;
7261
+ }
7262
+ if (!this.measureRubberBand) {
7263
+ this.measureRubberBand = this.createMeasureLine(last, target);
7264
+ this.measureRubberBand.material.opacity = 0.6;
7265
+ this.measureGroup.add(this.measureRubberBand);
7266
+ }
7267
+ this.measureRubberBand.geometry.setPositions([
7268
+ last.x, last.y, last.z,
7269
+ target.x, target.y, target.z,
7270
+ ]);
7271
+ this.measureRubberBand.visible = true;
7272
+ }
7273
+ createMeasureLabelTexture(text) {
7274
+ const canvas = document.createElement('canvas');
7275
+ canvas.width = 384;
7276
+ canvas.height = 128;
7277
+ const context = canvas.getContext('2d');
7278
+ const radius = 28;
7279
+ context.fillStyle = 'rgba(0, 0, 0, 0.78)';
7280
+ context.beginPath();
7281
+ context.moveTo(radius, 8);
7282
+ context.arcTo(canvas.width - 8, 8, canvas.width - 8, canvas.height - 8, radius);
7283
+ context.arcTo(canvas.width - 8, canvas.height - 8, 8, canvas.height - 8, radius);
7284
+ context.arcTo(8, canvas.height - 8, 8, 8, radius);
7285
+ context.arcTo(8, 8, canvas.width - 8, 8, radius);
7286
+ context.closePath();
7287
+ context.fill();
7288
+ context.fillStyle = '#FFFFFF';
7289
+ context.font = 'bold 60px sans-serif';
7290
+ context.textAlign = 'center';
7291
+ context.textBaseline = 'middle';
7292
+ context.fillText(text, canvas.width / 2, canvas.height / 2 + 4);
7293
+ return new THREE.CanvasTexture(canvas);
7294
+ }
7295
+ /** Removes everything drawn for a measurement and forgets its points. */
7296
+ clearMeasurement() {
7297
+ [...this.measureGroup.children].forEach((child) => {
7298
+ this.measureGroup.remove(child);
7299
+ child.geometry?.dispose?.();
7300
+ child.material?.map?.dispose?.();
7301
+ child.material?.dispose?.();
6795
7302
  });
7303
+ this.measureRubberBand = undefined;
7304
+ this.measurePoints = [];
7305
+ }
7306
+ /**
7307
+ * A picture of the scene with the measurement on it, as Matterport attaches to its own.
7308
+ *
7309
+ * Tags are left out, as they are there: the picture is about the measurement. It is taken straight
7310
+ * after a render because the drawing buffer is only guaranteed to hold the frame until the
7311
+ * browser presents it, and scaled down since it is uploaded and shown as a thumbnail.
7312
+ */
7313
+ captureMeasurementScreenshot() {
7314
+ const hidden = [];
7315
+ this.tagSprites.forEach((sprite) => {
7316
+ if (sprite.visible) {
7317
+ sprite.visible = false;
7318
+ hidden.push(sprite);
7319
+ }
7320
+ });
7321
+ this.renderer.render(this.scene, this.camera);
7322
+ const source = this.renderer.domElement;
7323
+ const scale = Math.min(1, 800 / source.width, 600 / source.height);
7324
+ const canvas = document.createElement('canvas');
7325
+ canvas.width = Math.max(1, Math.round(source.width * scale));
7326
+ canvas.height = Math.max(1, Math.round(source.height * scale));
7327
+ canvas.getContext('2d').drawImage(source, 0, 0, canvas.width, canvas.height);
7328
+ hidden.forEach((sprite) => {
7329
+ sprite.visible = true;
7330
+ });
7331
+ return canvas.toDataURL('image/jpeg', 0.85);
6796
7332
  }
6797
7333
  /** Arms tag placement: the next click on the model drops a tag instead of being ignored. */
6798
7334
  startTagPlacement(poiType = null) {
@@ -6823,6 +7359,7 @@ class ThreeJsService {
6823
7359
  const position = this.pickPointOnGpu(event);
6824
7360
  if (!position) {
6825
7361
  this.removePlacementPreview();
7362
+ this.updateMeasureRubberBand(null);
6826
7363
  this.renderer.domElement.style.cursor = 'not-allowed';
6827
7364
  return;
6828
7365
  }
@@ -6839,6 +7376,7 @@ class ThreeJsService {
6839
7376
  this.scaleTagForModel(this.placementPreview);
6840
7377
  }
6841
7378
  this.placementPreview.position.copy(position);
7379
+ this.updateMeasureRubberBand(position);
6842
7380
  }
6843
7381
  /**
6844
7382
  * The point under the cursor, read from the depth buffer instead of raycast, in tag space.
@@ -6940,9 +7478,250 @@ class ThreeJsService {
6940
7478
  * filter that happens to match nothing.
6941
7479
  */
6942
7480
  setVisibleTagTypes(types) {
7481
+ this.visibleTagTypes = types;
7482
+ this.tagSprites.forEach((sprite) => this.applyTagVisibility(sprite));
7483
+ }
7484
+ /** Says which capture is on screen, so the tags placed in it can be told from the rest. */
7485
+ setCurrentCapture(captureID) {
7486
+ this.currentCaptureID = captureID;
6943
7487
  this.tagSprites.forEach((sprite) => {
6944
- sprite.visible = types === null || types.includes(sprite.userData.poiType);
7488
+ this.parentTag(sprite);
7489
+ this.applyTagVisibility(sprite);
7490
+ });
7491
+ }
7492
+ /** Every tag registered in the scene, shown or not. */
7493
+ getTags() {
7494
+ const candidates = new Set(this.calibrationCandidates());
7495
+ return [...this.tagSprites.values()].map((sprite) => ({
7496
+ id: sprite.name,
7497
+ label: sprite.userData.label ?? null,
7498
+ poiType: sprite.userData.poiType ?? null,
7499
+ visible: sprite.visible,
7500
+ calibratable: candidates.has(sprite),
7501
+ }));
7502
+ }
7503
+ /**
7504
+ * How far the tags from elsewhere sit from the model, measured in model radii. Null without both.
7505
+ *
7506
+ * Before a model is calibrated those tags are wherever their own coordinates put them, which for
7507
+ * a model with a different origin can be a long way off - too far to find, let alone click.
7508
+ */
7509
+ getTagDistanceFromModel() {
7510
+ if (!this.modelRoot || !this.modelWorldRadius) {
7511
+ return null;
7512
+ }
7513
+ const centroid = new THREE.Vector3();
7514
+ const candidates = this.calibrationCandidates();
7515
+ candidates.forEach((sprite) => centroid.add(sprite.getWorldPosition(new THREE.Vector3())));
7516
+ const count = candidates.length;
7517
+ if (count === 0) {
7518
+ return null;
7519
+ }
7520
+ // The model is centred on the world origin when it is placed.
7521
+ return centroid.divideScalar(count).length() / this.modelWorldRadius;
7522
+ }
7523
+ /**
7524
+ * Hangs a tag from the group its coordinates belong to: outside the calibration when they are in
7525
+ * this model's own coordinates, under it otherwise.
7526
+ */
7527
+ parentTag(sprite) {
7528
+ const parent = sprite.userData.frame === 'model' && this.isOwnedByVisit(sprite)
7529
+ ? this.modelTagGroup
7530
+ : this.tagGroup;
7531
+ if (sprite.parent !== parent) {
7532
+ parent.add(sprite);
7533
+ }
7534
+ }
7535
+ /**
7536
+ * The tags a calibration can be worked out from: those that came from somewhere else, whether or
7537
+ * not they are on show - before calibrating, most of them will not be.
7538
+ *
7539
+ * Tags placed in this model are where they are because someone clicked there; matching them to
7540
+ * "where they really are" would be circular. A tag in another model's own coordinates cannot be
7541
+ * placed here at all. And a tag already used in this calibration cannot be used again - two
7542
+ * pairs from one tag carry no more than one.
7543
+ */
7544
+ calibrationCandidates() {
7545
+ const used = new Set(this.alignmentPairs.map((pair) => pair.tagId));
7546
+ return [...this.tagSprites.values()].filter((sprite) => sprite.parent === this.tagGroup
7547
+ && sprite.userData.frame !== 'model'
7548
+ && !this.isOwnedByVisit(sprite)
7549
+ && !used.has(sprite.name));
7550
+ }
7551
+ /** How many tags there are to calibrate with, not counting those already used. */
7552
+ getCalibrationCandidateCount() {
7553
+ return this.calibrationCandidates().length;
7554
+ }
7555
+ /**
7556
+ * How many tags from elsewhere are not on this model, and so not shown on it.
7557
+ *
7558
+ * On a model that has not been calibrated, this is what suggests it should be: the tags are out
7559
+ * there, just not where the model is.
7560
+ */
7561
+ getOffModelTagCount() {
7562
+ return this.calibrationCandidates().filter((sprite) => !this.isOnModel(sprite)).length;
7563
+ }
7564
+ /** Whether the tag was placed in the visit on screen. */
7565
+ isOwnedByVisit(sprite) {
7566
+ return !!sprite.userData.originCaptureID
7567
+ && sprite.userData.originCaptureID === this.currentCaptureID;
7568
+ }
7569
+ /**
7570
+ * A tag shows when its type is not filtered out and it belongs on this model.
7571
+ *
7572
+ * One place decides, because the two used to set `visible` separately and whichever ran last
7573
+ * undid the other.
7574
+ */
7575
+ applyTagVisibility(sprite) {
7576
+ const typeAllowed = this.visibleTagTypes === null
7577
+ || this.visibleTagTypes.includes(sprite.userData.poiType);
7578
+ sprite.visible = typeAllowed && this.belongsOnThisModel(sprite);
7579
+ }
7580
+ /**
7581
+ * Whether a tag has a place on this model.
7582
+ *
7583
+ * Every tag of the space is offered to every model - there is one set of tags, not one per model.
7584
+ * What a model shows of it is the part that sits on the model.
7585
+ */
7586
+ belongsOnThisModel(sprite) {
7587
+ // Placed here by a click on this very surface.
7588
+ if (this.isOwnedByVisit(sprite)) {
7589
+ return true;
7590
+ }
7591
+ // Placed in another model before that one was calibrated: its coordinates are that model's own
7592
+ // and say nothing about where it would be in this one.
7593
+ if (sprite.userData.frame === 'model') {
7594
+ return false;
7595
+ }
7596
+ // Calibrating is matching tags to where they belong, so they all have to be on offer.
7597
+ if (this.aligning) {
7598
+ return true;
7599
+ }
7600
+ return this.isOnModel(sprite);
7601
+ }
7602
+ /** Whether a tag, drawn where its coordinates and the calibration put it, sits on the model. */
7603
+ isOnModel(sprite) {
7604
+ if (!this.surfaceCells) {
7605
+ // No model to be on yet.
7606
+ return false;
7607
+ }
7608
+ const parent = sprite.parent ?? this.tagGroup;
7609
+ parent.updateMatrix();
7610
+ // The grid is in the model's own units; the calibration's scale is how many of those a metre is.
7611
+ const reach = ThreeJsService.ON_MODEL_METRES * (parent.scale.x || 1);
7612
+ return this.isNearSurface(sprite.position.clone().applyMatrix4(parent.matrix), reach);
7613
+ }
7614
+ /** Re-decides every tag's visibility: the model, the calibration or the mode changed. */
7615
+ refreshTagVisibility() {
7616
+ this.tagSprites.forEach((sprite) => this.applyTagVisibility(sprite));
7617
+ }
7618
+ surfaceKey(x, y, z) {
7619
+ const span = ThreeJsService.SURFACE_GRID_SPAN;
7620
+ const pad = ThreeJsService.SURFACE_GRID_PAD;
7621
+ const [i, j, k] = [x + pad, y + pad, z + pad];
7622
+ if (i < 0 || j < 0 || k < 0 || i >= span || j >= span || k >= span) {
7623
+ return -1;
7624
+ }
7625
+ return i + span * (j + span * k);
7626
+ }
7627
+ /**
7628
+ * Lays the grid over the model's surface.
7629
+ *
7630
+ * Runs once per model, in one pass over its triangles. A triangle smaller than a cell is marked
7631
+ * by its centre, which is every triangle of a scanned model; a larger one - the two triangles of
7632
+ * a drawn wall - is sampled across its face, or the middle of the wall would read as empty.
7633
+ */
7634
+ buildSurfaceGrid(object, box) {
7635
+ const size = box.getSize(new THREE.Vector3());
7636
+ const longest = Math.max(size.x, size.y, size.z);
7637
+ if (!(longest > 0)) {
7638
+ this.surfaceCells = null;
7639
+ return;
7640
+ }
7641
+ // A quarter of a metre where the model is in metres; coarser on a very large one, so the grid
7642
+ // stays addressable and its memory bounded.
7643
+ const cell = Math.max(0.25, longest / 384);
7644
+ const origin = box.min.clone();
7645
+ const cells = new Set();
7646
+ const mark = (point) => {
7647
+ const key = this.surfaceKey(Math.floor((point.x - origin.x) / cell), Math.floor((point.y - origin.y) / cell), Math.floor((point.z - origin.z) / cell));
7648
+ if (key >= 0) {
7649
+ cells.add(key);
7650
+ }
7651
+ };
7652
+ let triangleCount = 0;
7653
+ object.updateMatrixWorld(true);
7654
+ object.traverse((child) => {
7655
+ const position = child.isMesh && child.geometry?.attributes?.position;
7656
+ if (position) {
7657
+ triangleCount += (child.geometry.index ? child.geometry.index.count : position.count) / 3;
7658
+ }
7659
+ });
7660
+ // Past a few million triangles they are far smaller than a cell, and every cell is hit many
7661
+ // times over; a sample marks the same cells.
7662
+ const stride = Math.max(1, Math.ceil(triangleCount / 3000000));
7663
+ const a = new THREE.Vector3();
7664
+ const b = new THREE.Vector3();
7665
+ const c = new THREE.Vector3();
7666
+ const point = new THREE.Vector3();
7667
+ object.traverse((child) => {
7668
+ const position = child.isMesh && child.geometry?.attributes?.position;
7669
+ if (!position) {
7670
+ return;
7671
+ }
7672
+ const index = child.geometry.index;
7673
+ const count = (index ? index.count : position.count) / 3;
7674
+ for (let triangle = 0; triangle < count; triangle += stride) {
7675
+ const offset = triangle * 3;
7676
+ a.fromBufferAttribute(position, index ? index.getX(offset) : offset).applyMatrix4(child.matrixWorld);
7677
+ b.fromBufferAttribute(position, index ? index.getX(offset + 1) : offset + 1).applyMatrix4(child.matrixWorld);
7678
+ c.fromBufferAttribute(position, index ? index.getX(offset + 2) : offset + 2).applyMatrix4(child.matrixWorld);
7679
+ const edge = Math.max(a.distanceTo(b), b.distanceTo(c), c.distanceTo(a));
7680
+ if (edge <= cell) {
7681
+ mark(point.copy(a).add(b).add(c).multiplyScalar(1 / 3));
7682
+ continue;
7683
+ }
7684
+ // Half-cell steps across the face. Capped, so one enormous triangle cannot stall the load;
7685
+ // the reach a tag is tested with spans several cells and bridges what the cap leaves.
7686
+ const steps = Math.min(96, Math.ceil((2 * edge) / cell));
7687
+ for (let i = 0; i <= steps; i += 1) {
7688
+ for (let j = 0; j <= steps - i; j += 1) {
7689
+ point.copy(a)
7690
+ .addScaledVector(b.clone().sub(a), i / steps)
7691
+ .addScaledVector(c.clone().sub(a), j / steps);
7692
+ mark(point);
7693
+ }
7694
+ }
7695
+ }
6945
7696
  });
7697
+ this.surfaceCells = cells;
7698
+ this.surfaceOrigin.copy(origin);
7699
+ this.surfaceCellSize = cell;
7700
+ }
7701
+ /** Whether some part of the model's surface lies within `reach` of a point, in model units. */
7702
+ isNearSurface(point, reach) {
7703
+ const cell = this.surfaceCellSize;
7704
+ const origin = this.surfaceOrigin;
7705
+ const x = (point.x - origin.x) / cell;
7706
+ const y = (point.y - origin.y) / cell;
7707
+ const z = (point.z - origin.z) / cell;
7708
+ // In cells. Capped: beyond this the calibration's scale makes a metre so many cells that the
7709
+ // search would dwarf the model, and a tag that far off is not on it by any reading.
7710
+ const span = Math.min(16, Math.ceil(reach / cell));
7711
+ // A cell is "within reach" when its centre is, give or take the half-diagonal of the cell.
7712
+ const limit = (reach / cell + 0.87) ** 2;
7713
+ const [cx, cy, cz] = [Math.floor(x), Math.floor(y), Math.floor(z)];
7714
+ for (let k = cz - span; k <= cz + span; k += 1) {
7715
+ for (let j = cy - span; j <= cy + span; j += 1) {
7716
+ for (let i = cx - span; i <= cx + span; i += 1) {
7717
+ const distance = (i + 0.5 - x) ** 2 + (j + 0.5 - y) ** 2 + (k + 0.5 - z) ** 2;
7718
+ if (distance <= limit && this.surfaceCells.has(this.surfaceKey(i, j, k))) {
7719
+ return true;
7720
+ }
7721
+ }
7722
+ }
7723
+ }
7724
+ return false;
6946
7725
  }
6947
7726
  /**
6948
7727
  * Moves the camera to look at a tag, mirroring Matterport's goToTag.
@@ -7073,8 +7852,11 @@ class ThreeJsService {
7073
7852
  this.tagGroup.position.set(translation?.x ?? 0, translation?.y ?? 0, translation?.z ?? 0);
7074
7853
  this.tagGroup.rotation.set(rotation?.x ?? 0, rotation?.y ?? 0, rotation?.z ?? 0);
7075
7854
  this.tagGroup.scale.setScalar(scale ?? 1);
7076
- // Tag size is derived from the world scale it inherits, which this call just changed.
7855
+ this.tagGroup.updateMatrix();
7856
+ // Tag size is derived from the world scale it inherits, which this call just changed - and so
7857
+ // is which tags land on the model.
7077
7858
  this.tagSprites.forEach((sprite) => this.scaleTagForModel(sprite));
7859
+ this.refreshTagVisibility();
7078
7860
  }
7079
7861
  /**
7080
7862
  * Adds a tag, or moves/restyles the one already registered for this element.
@@ -7089,6 +7871,10 @@ class ThreeJsService {
7089
7871
  existing.position.set(tag.position.x, tag.position.y, tag.position.z);
7090
7872
  existing.userData.poiType = tag.poiType ?? existing.userData.poiType;
7091
7873
  existing.userData.label = tag.label ?? existing.userData.label;
7874
+ existing.userData.originCaptureID = tag.originCaptureID ?? existing.userData.originCaptureID;
7875
+ existing.userData.frame = tag.frame ?? existing.userData.frame;
7876
+ this.parentTag(existing);
7877
+ this.applyTagVisibility(existing);
7092
7878
  return;
7093
7879
  }
7094
7880
  const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
@@ -7101,10 +7887,13 @@ class ThreeJsService {
7101
7887
  sprite.name = tag.id;
7102
7888
  sprite.userData.poiType = tag.poiType ?? null;
7103
7889
  sprite.userData.label = tag.label ?? null;
7890
+ sprite.userData.originCaptureID = tag.originCaptureID ?? null;
7891
+ sprite.userData.frame = tag.frame ?? null;
7104
7892
  sprite.position.set(tag.position.x, tag.position.y, tag.position.z);
7105
7893
  sprite.renderOrder = 999;
7894
+ this.parentTag(sprite);
7106
7895
  this.scaleTagForModel(sprite);
7107
- this.tagGroup.add(sprite);
7896
+ this.applyTagVisibility(sprite);
7108
7897
  this.tagSprites.set(tag.id, sprite);
7109
7898
  if (tag.iconUrl) {
7110
7899
  this.applyTagIcon(sprite, tag.iconUrl);
@@ -7116,7 +7905,7 @@ class ThreeJsService {
7116
7905
  if (!sprite) {
7117
7906
  return;
7118
7907
  }
7119
- this.tagGroup.remove(sprite);
7908
+ sprite.parent?.remove(sprite);
7120
7909
  sprite.material.map?.dispose();
7121
7910
  sprite.material.dispose();
7122
7911
  this.tagSprites.delete(elementId);
@@ -7157,7 +7946,8 @@ class ThreeJsService {
7157
7946
  * size on screen whatever the model's units are.
7158
7947
  */
7159
7948
  scaleTagForModel(sprite) {
7160
- const worldScale = this.tagGroup.getWorldScale(new THREE.Vector3()).x || 1;
7949
+ // Its own parent's scale: tags in the model's coordinates do not inherit the calibration's.
7950
+ const worldScale = (sprite.parent ?? this.tagGroup).getWorldScale(new THREE.Vector3()).x || 1;
7161
7951
  const size = 0.4 / worldScale;
7162
7952
  sprite.scale.set(size, size, 1);
7163
7953
  }
@@ -7216,7 +8006,7 @@ class ThreeJsService {
7216
8006
  this.removePlacementPreview();
7217
8007
  // Tags first: they draw on top of the model, so they have to answer the pointer first too.
7218
8008
  const hoveredTag = this.raycaster
7219
- .intersectObjects(this.tagGroup.children, false)
8009
+ .intersectObjects([...this.tagSprites.values()], false)
7220
8010
  .find((hit) => hit.object.visible)?.object;
7221
8011
  if (hoveredTag) {
7222
8012
  this.renderer.domElement.style.cursor = 'pointer';
@@ -7255,6 +8045,15 @@ class ThreeJsService {
7255
8045
  this.handleAlignmentClick(event);
7256
8046
  return;
7257
8047
  }
8048
+ // Measuring takes every click as a point, the same way placement takes it as a position.
8049
+ if (this.measuring) {
8050
+ const placement = this.pickPlacementOnModel(event);
8051
+ if (placement) {
8052
+ const { x, y, z } = placement.position;
8053
+ this.addMeasurePoint(new THREE.Vector3(x, y, z));
8054
+ }
8055
+ return;
8056
+ }
7258
8057
  // Placement wins over everything: while armed, a click means "put it here", not "open that".
7259
8058
  if (this.placingTag) {
7260
8059
  const placement = this.pickPlacementOnModel(event);
@@ -7270,7 +8069,7 @@ class ThreeJsService {
7270
8069
  }
7271
8070
  this.raycaster.setFromCamera(this.mouse, this.camera);
7272
8071
  // Same precedence as hover: a tag drawn over the model claims the click.
7273
- const tagHits = this.raycaster.intersectObjects(this.tagGroup.children, false)
8072
+ const tagHits = this.raycaster.intersectObjects([...this.tagSprites.values()], false)
7274
8073
  .filter((hit) => hit.object.visible);
7275
8074
  if (tagHits.length > 0) {
7276
8075
  const tag = tagHits[0].object;
@@ -7305,6 +8104,10 @@ class ThreeJsService {
7305
8104
  this.cancelTagPlacement();
7306
8105
  return;
7307
8106
  }
8107
+ if (this.measuring) {
8108
+ this.cancelMeasuring();
8109
+ return;
8110
+ }
7308
8111
  if (this.aligning) {
7309
8112
  this.cancelTagAlignment();
7310
8113
  }
@@ -7332,6 +8135,8 @@ class ThreeJsService {
7332
8135
  }
7333
8136
  this.discardCurrentLoad();
7334
8137
  this.clearTags();
8138
+ this.measuring = false;
8139
+ this.clearMeasurement();
7335
8140
  if (this.renderer) {
7336
8141
  this.renderer.dispose();
7337
8142
  }
@@ -7349,16 +8154,481 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
7349
8154
  }]
7350
8155
  }], ctorParameters: () => [{ type: i0.NgZone }] });
7351
8156
 
7352
- /* eslint-disable no-await-in-loop */
7353
- /* eslint-disable camelcase */
7354
- /* eslint-disable no-underscore-dangle */
7355
- /* eslint-disable class-methods-use-this */
7356
- class ViewerService {
7357
- sdkKey;
7358
- config;
7359
- poiService;
8157
+ class CaptureService {
8158
+ API; //AWS
8159
+ constructor(apiInjected) {
8160
+ this.API = apiInjected;
8161
+ }
8162
+ async getCapturesBySpace(spaceID) {
8163
+ return this.API.CapturesBySpace(spaceID).then((cap) => cap.items);
8164
+ }
8165
+ async getCapture(captureID) {
8166
+ return this.API.__proto__.GetCapture(captureID);
8167
+ }
8168
+ async deleteCapture(captureID) {
8169
+ return this.API.__proto__.DeleteCapture({ id: captureID });
8170
+ }
8171
+ async updateCapture(capture) {
8172
+ return this.API.__proto__.UpdateCapture(capture);
8173
+ }
8174
+ async createCapture(capture) {
8175
+ return this.API.__proto__.CreateCapture(capture);
8176
+ }
8177
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CaptureService, deps: [{ token: 'currentAPIService' }], target: i0.ɵɵFactoryTarget.Injectable });
8178
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CaptureService, providedIn: "root" });
8179
+ }
8180
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CaptureService, decorators: [{
8181
+ type: Injectable,
8182
+ args: [{
8183
+ providedIn: "root",
8184
+ }]
8185
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
8186
+ type: Inject,
8187
+ args: ['currentAPIService']
8188
+ }] }] });
8189
+
8190
+ /* eslint-disable class-methods-use-this */
8191
+ class ZoneService {
8192
+ userService;
8193
+ API; //AWS APIService
8194
+ constructor(apiInjected, userService) {
8195
+ this.userService = userService;
8196
+ this.API = apiInjected;
8197
+ }
8198
+ async getZonesBySpace(spaceID) {
8199
+ const list = [];
8200
+ if (this.API.__proto__.hasOwnProperty('ZonesBySpace')) {
8201
+ // Paginated: without a nextToken loop, only the first page (100 zones) was returned and
8202
+ // the sweeps of the other zones silently disappeared.
8203
+ let nextToken = null;
8204
+ do {
8205
+ const zones = await this.API.__proto__.ZonesBySpace(spaceID, null, null, 1000, nextToken);
8206
+ for (const zone of zones?.items || []) {
8207
+ const zoneObject = { ...zone };
8208
+ zoneObject.virtual = this.zoneIsVirtual(zone);
8209
+ list.push(zoneObject);
8210
+ }
8211
+ nextToken = zones?.nextToken ?? null;
8212
+ } while (nextToken);
8213
+ return list;
8214
+ }
8215
+ else {
8216
+ throw new Error('ZonesBySpace not implemented');
8217
+ }
8218
+ }
8219
+ async getZonesBySpaceForUser(spaceID) {
8220
+ const missionForLocation = this.userService.currentMission(spaceID);
8221
+ const list = [];
8222
+ if (missionForLocation && missionForLocation.zoneID) {
8223
+ const allowedZone = await this.getZone(missionForLocation.zoneID);
8224
+ allowedZone.virtual = this.zoneIsVirtual(allowedZone);
8225
+ if (!allowedZone.parent) {
8226
+ // the whole building is authorized
8227
+ return this.getZonesBySpace(spaceID);
8228
+ }
8229
+ list.push(allowedZone);
8230
+ // allow all children zones of an allowed parent
8231
+ const children = (await this.API.__proto__.ZonesByZone(allowedZone.id))
8232
+ .items;
8233
+ if (children.length > 0) {
8234
+ for (const child of children) {
8235
+ const childZone = { ...child };
8236
+ childZone.virtual = this.zoneIsVirtual(child);
8237
+ list.push(childZone);
8238
+ }
8239
+ }
8240
+ // allow the parent Floor of the allowed zone
8241
+ if (allowedZone.parent.layer &&
8242
+ allowedZone.parent.layer.name === 'FLOOR') {
8243
+ list.push(allowedZone.parent);
8244
+ }
8245
+ }
8246
+ else {
8247
+ return this.getZonesBySpace(spaceID);
8248
+ }
8249
+ return list;
8250
+ }
8251
+ async filterZonesIDsForUser(zones) {
8252
+ const { spaceID } = zones[0];
8253
+ const zoneIDs = new Set(zones.map((zone) => zone.id));
8254
+ const allowedZones = await this.getZonesBySpaceForUser(spaceID);
8255
+ const filteredZones = allowedZones.filter((zone) => zoneIDs.has(zone.id));
8256
+ return filteredZones;
8257
+ }
8258
+ async deleteZone(zoneID) {
8259
+ return this.API.__proto__.DeleteZone({ id: zoneID });
8260
+ }
8261
+ async getZonesForSweepForSpace(sweepID, spaceID) {
8262
+ const zonesSpace = (await this.API.__proto__.ZonesBySpace(spaceID)).items;
8263
+ return zonesSpace.filter((zone) => zone.sweepIDs && zone.sweepIDs.includes(sweepID));
8264
+ }
8265
+ // // eslint-disable-next-line class-methods-use-this
8266
+ sortZonesFromChildToParent(zonesToSort) {
8267
+ // get default
8268
+ const defaultZone = zonesToSort.find((z) => !z.parentID);
8269
+ if (defaultZone) {
8270
+ const indexOf = zonesToSort.indexOf(defaultZone);
8271
+ zonesToSort.splice(indexOf, 1);
8272
+ }
8273
+ const zones = zonesToSort.sort((a, b) => {
8274
+ if (a.parentID === b.id) {
8275
+ return -1;
8276
+ }
8277
+ return 0;
8278
+ });
8279
+ // so far by number of navigation points in them
8280
+ // const zones = zonesToSort.sort(
8281
+ // (a, b) => a.navigations.items.length - b.navigations.items.length,
8282
+ // );
8283
+ if (defaultZone) {
8284
+ zones.push(defaultZone);
8285
+ }
8286
+ return zones;
8287
+ }
8288
+ async getZonesForObject(object) {
8289
+ if (object.pois.items && object.pois.items.length > 0) {
8290
+ const [poi] = object.pois.items;
8291
+ if (poi.matterportSweepID) {
8292
+ const zones = await this.getZonesForSweepForSpace(poi.matterportSweepID, object.spaceID);
8293
+ return zones;
8294
+ }
8295
+ }
8296
+ return null;
8297
+ }
8298
+ async createVirtualZone(zoneInput) {
8299
+ zoneInput.sweepIDs = [];
8300
+ const zone = await this.API.__proto__.CreateZone(zoneInput);
8301
+ console.log('virtual zone created', zone);
8302
+ return zone;
8303
+ }
8304
+ zoneIsVirtual(zone) {
8305
+ if (!zone.sweepIDs) {
8306
+ return true;
8307
+ }
8308
+ if ((zone.tags && zone.tags.includes('virtual')) ||
8309
+ zone.sweepIDs.length === 0) {
8310
+ return true;
8311
+ }
8312
+ return false;
8313
+ }
8314
+ async getModel3DForZone(zoneID) {
8315
+ const zone = await this.API.__proto__.GetZone(zoneID);
8316
+ if (zone.nodeIDs) {
8317
+ const node = await this.API.__proto__.GetNode(zone.nodeIDs[0]);
8318
+ return node.model3D;
8319
+ }
8320
+ return null;
8321
+ }
8322
+ async getZone(zoneID) {
8323
+ return this.API.__proto__.GetZone(zoneID);
8324
+ }
8325
+ async getDefaultZoneForSpace(spaceID) {
8326
+ return this.getZonesBySpace(spaceID).then(async (zones) => {
8327
+ const zonesBuilding = zones.filter((zone) => zone.layer && zone.layer.name === 'BUILDING');
8328
+ if (zonesBuilding.length === 1) {
8329
+ return zonesBuilding[0];
8330
+ }
8331
+ const parentZone = zones.find((zone) => !zone.parentID);
8332
+ return parentZone;
8333
+ });
8334
+ }
8335
+ async create(input) {
8336
+ return this.API.__proto__.CreateZone(input);
8337
+ }
8338
+ // async getSortedZonesForSweepForUser(
8339
+ // sweep: string,
8340
+ // spaceID: string,
8341
+ // ): Promise<Zone[]> {
8342
+ // if (!spaceID) {
8343
+ // return [];
8344
+ // }
8345
+ // const zones = await this.getZonesForSweepForSpace(sweep, spaceID);
8346
+ // if (zones.length === 0) {
8347
+ // return [];
8348
+ // }
8349
+ // const filteredZones = await this.filterZonesIDsForUser(zones);
8350
+ // if (filteredZones.length > 0) {
8351
+ // let zonesSorted = this.sortZonesFromChildToParent(filteredZones);
8352
+ // zonesSorted = zonesSorted.filter((zone) => zone !== undefined);
8353
+ // return zonesSorted;
8354
+ // }
8355
+ // return [];
8356
+ // }
8357
+ async updateZone(input) {
8358
+ return this.API.__proto__.UpdateZone(input);
8359
+ }
8360
+ async getZonesForNodeForSpace(nodeID, spaceID) {
8361
+ return this.API.__proto__.ZonesBySpace(spaceID).then(async (zones) => {
8362
+ const filteredZones = zones.items.filter((zone) => zone.nodeIDs && zone.nodeIDs.includes(nodeID));
8363
+ return filteredZones;
8364
+ });
8365
+ }
8366
+ /**
8367
+ * Gets the Zone (layer FLOOR) for a matterport sweep ID in a given Space
8368
+ * @param sweepID
8369
+ * @param spaceID
8370
+ * @returns Zone
8371
+ */
8372
+ async getFloorZoneForSweepInSpace(sweepID, spaceID) {
8373
+ const zones = await this.getZonesForSweepForSpace(sweepID, spaceID);
8374
+ const floors = zones.filter((zone) => zone.layer && zone.layer.name.includes('FLOOR'));
8375
+ return floors.length > 0 ? floors[0] : null;
8376
+ }
8377
+ /**
8378
+ * Get zones with audio track for entire space
8379
+ * @param spaceID
8380
+ * @returns Promise<Zone[]>
8381
+ */
8382
+ async getAudioZonesForSpace(spaceID) {
8383
+ const allZones = await this.getZonesBySpace(spaceID);
8384
+ return allZones.filter((zone) => zone.audioID);
8385
+ }
8386
+ /**
8387
+ * Get audio zones for user (within allowed zones)
8388
+ * @param spaceID
8389
+ * @returns Promise<Zone[]>
8390
+ */
8391
+ async getAudioZonesForUserForSpace(spaceID) {
8392
+ const userZones = await this.getZonesBySpaceForUser(spaceID);
8393
+ return userZones.filter((zone) => zone.audioID);
8394
+ }
8395
+ /**
8396
+ * Get Zones by Parent Zone ID
8397
+ * @param parentZoneID string
8398
+ * @returns Promise<Zone[]>
8399
+ */
8400
+ async getZonesByParentZone(parentZoneID) {
8401
+ return this.API.__proto__
8402
+ .ZonesByZone(parentZoneID)
8403
+ .then((res) => res.items);
8404
+ }
8405
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ZoneService, deps: [{ token: 'currentAPIService' }, { token: BaseUserService }], target: i0.ɵɵFactoryTarget.Injectable });
8406
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ZoneService, providedIn: 'root' });
8407
+ }
8408
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ZoneService, decorators: [{
8409
+ type: Injectable,
8410
+ args: [{
8411
+ providedIn: 'root',
8412
+ }]
8413
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
8414
+ type: Inject,
8415
+ args: ['currentAPIService']
8416
+ }] }, { type: BaseUserService }] });
8417
+
8418
+ class NavigationService {
8419
+ userService;
8420
+ zoneService;
8421
+ API;
8422
+ constructor(apiInjected, userService, zoneService) {
8423
+ this.userService = userService;
8424
+ this.zoneService = zoneService;
8425
+ this.API = apiInjected;
8426
+ }
8427
+ async createNavigation(navInput) {
8428
+ return this.API.__proto__.CreateNavigation(navInput);
8429
+ }
8430
+ async deleteNavigation(id) {
8431
+ return this.API.__proto__.DeleteNavigation({ id });
8432
+ }
8433
+ async getNavigationsForNode(nodeID) {
8434
+ const limit = 200;
8435
+ let navigationsTokenDict = { navigations: [], nextToken: null };
8436
+ do {
8437
+ navigationsTokenDict =
8438
+ await this.mergeAPIResponseWithNextTokenWithLimit(nodeID, limit, navigationsTokenDict);
8439
+ } while (navigationsTokenDict.nextToken);
8440
+ return navigationsTokenDict.navigations;
8441
+ }
8442
+ async mergeAPIResponseWithNextTokenWithLimit(nodeID, limit, navigationsTokenDict) {
8443
+ return this.API.__proto__.NavigationsByNode(nodeID, null, null, limit, navigationsTokenDict.nextToken).then((response) => {
8444
+ const navigations = response.items;
8445
+ const navObject = {
8446
+ navigations: [...navigationsTokenDict.navigations, ...navigations],
8447
+ nextToken: response.nextToken,
8448
+ };
8449
+ return navObject;
8450
+ });
8451
+ }
8452
+ async getNavigation(id) {
8453
+ return this.API.__proto__.GetNavigation(id);
8454
+ }
8455
+ async getSweepsIDsForSpace(spaceID) {
8456
+ const zones = await this.zoneService.getZonesBySpace(spaceID);
8457
+ const zonesWithSweeps = zones.filter((zone) => zone.sweepIDs && zone.sweepIDs.length > 0);
8458
+ const allSweeps = zonesWithSweeps.flatMap((zone) => zone.sweepIDs);
8459
+ return [...new Set(allSweeps)];
8460
+ }
8461
+ async getAllowedSweepsIDsForUserInSpace(spaceID) {
8462
+ const missionsForSpace = this.userService.currentMissions.filter((mis) => mis.spaceID === spaceID);
8463
+ console.log(missionsForSpace);
8464
+ let navIDs = [];
8465
+ if (this.userService.userHasAccessToAllZonesInSpace(spaceID)) {
8466
+ console.log(1);
8467
+ return this.getSweepsIDsForSpace(spaceID);
8468
+ }
8469
+ await Promise.all(missionsForSpace.map(async (mission) => {
8470
+ navIDs = [...navIDs, ...mission.zone.sweepIDs];
8471
+ }));
8472
+ console.log('final navids = ', navIDs);
8473
+ return navIDs;
8474
+ }
8475
+ async getForbiddenNavigationIDsForUserInSpace(spaceID) {
8476
+ const allowed = await this.getAllowedSweepsIDsForUserInSpace(spaceID);
8477
+ const allNavs = await this.getSweepsIDsForSpace(spaceID);
8478
+ const difference = allNavs.filter((x) => !allowed.includes(x));
8479
+ return difference.length > 0 ? difference : null;
8480
+ }
8481
+ async getNavigationsForMatterportIDForSpace(matterportID, spaceID) {
8482
+ return this.API.__proto__.NavigationsByMatterportIDBySpace(matterportID, {
8483
+ eq: spaceID,
8484
+ }).then((navs) => navs.items);
8485
+ }
8486
+ async getNavigationsForZone(zone) {
8487
+ const responseNavs = [];
8488
+ await Promise.all(zone.sweepIDs.map(async (sweepID) => {
8489
+ const navs = await this.getNavigationsForMatterportIDForSpace(sweepID, zone.spaceID);
8490
+ if (navs.length > 0) {
8491
+ responseNavs.push(navs[0]);
8492
+ }
8493
+ }));
8494
+ return responseNavs;
8495
+ }
8496
+ async updateNavigation(navInput) {
8497
+ return this.API.__proto__.UpdateNavigation(navInput);
8498
+ }
8499
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NavigationService, deps: [{ token: 'currentAPIService' }, { token: BaseUserService }, { token: ZoneService }], target: i0.ɵɵFactoryTarget.Injectable });
8500
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NavigationService, providedIn: "root" });
8501
+ }
8502
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NavigationService, decorators: [{
8503
+ type: Injectable,
8504
+ args: [{
8505
+ providedIn: "root",
8506
+ }]
8507
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
8508
+ type: Inject,
8509
+ args: ['currentAPIService']
8510
+ }] }, { type: BaseUserService }, { type: ZoneService }] });
8511
+
8512
+ /**
8513
+ * Finds the Matterport sweep a point in a space sits closest to.
8514
+ *
8515
+ * A tag placed in Matterport is recorded with the sweep it was placed from, and much of the
8516
+ * Matterport side keys on that: which floor plan draws the tag, which zone it belongs to, which
8517
+ * floor it shows on. A tag placed in the Three.js viewer has no sweep, so it was missing from all
8518
+ * of them. Resolving the nearest one at creation - what the tag copy tool already does for copied
8519
+ * tags - lets every one of those readers work unchanged instead of each growing its own exception.
8520
+ */
8521
+ class SweepLocatorService {
8522
+ captureService;
8523
+ navigationService;
8524
+ /**
8525
+ * Sweeps sit a few metres apart, so a point on a scanned surface always has one nearby. Anything
8526
+ * further means the coordinates are not in the Matterport capture's space - a model that was
8527
+ * never calibrated - and a sweep picked from that far would file the tag under an arbitrary
8528
+ * floor. Leaving it without one is the honest answer there.
8529
+ */
8530
+ static MAX_DISTANCE_METRES = 10;
8531
+ /** Sweep positions per space. They only change on import, so one load per session is enough. */
8532
+ sweepsBySpace = new Map();
8533
+ constructor(captureService, navigationService) {
8534
+ this.captureService = captureService;
8535
+ this.navigationService = navigationService;
8536
+ }
8537
+ /**
8538
+ * The sweep nearest to a point, or null when the space has none within reach.
8539
+ *
8540
+ * Never throws: this only refines where a tag is filed, and a tag that cannot be filed is still
8541
+ * better saved without a sweep than not saved at all.
8542
+ */
8543
+ async findNearestSweep(point, spaceID) {
8544
+ if (!point || !spaceID) {
8545
+ return null;
8546
+ }
8547
+ try {
8548
+ return this.nearestSweep(point, await this.loadSweeps(spaceID));
8549
+ }
8550
+ catch (error) {
8551
+ console.warn(`SweepLocatorService: could not resolve a sweep in space ${spaceID}`, error);
8552
+ return null;
8553
+ }
8554
+ }
8555
+ /** The sweep closest to a point, or null when none is within reach. */
8556
+ nearestSweep(point, sweeps) {
8557
+ let nearest = null;
8558
+ let nearestDistance = SweepLocatorService.MAX_DISTANCE_METRES;
8559
+ for (const sweep of sweeps) {
8560
+ const distance = Math.hypot(sweep.x - point.x, sweep.y - point.y, sweep.z - point.z);
8561
+ if (distance <= nearestDistance) {
8562
+ nearestDistance = distance;
8563
+ nearest = sweep.sweepID;
8564
+ }
8565
+ }
8566
+ return nearest;
8567
+ }
8568
+ loadSweeps(spaceID) {
8569
+ let sweeps = this.sweepsBySpace.get(spaceID);
8570
+ if (!sweeps) {
8571
+ sweeps = this.fetchSweeps(spaceID);
8572
+ this.sweepsBySpace.set(spaceID, sweeps);
8573
+ // A failed load must not be remembered as "this space has no sweeps".
8574
+ sweeps.catch(() => this.sweepsBySpace.delete(spaceID));
8575
+ }
8576
+ return sweeps;
8577
+ }
8578
+ async fetchSweeps(spaceID) {
8579
+ const captures = await this.captureService.getCapturesBySpace(spaceID);
8580
+ // A 3D model is imported with one navigation entry of its own, a placeholder sitting at the
8581
+ // origin. It is not a sweep, and counting it would make it the "nearest" for any tag close to
8582
+ // the origin.
8583
+ const nodeIDs = (captures ?? [])
8584
+ .filter((capture) => capture.viewer !== CaptureViewer.THREEJS)
8585
+ .reduce((all, capture) => all.concat(capture.nodes?.items ?? []), [])
8586
+ .map((node) => node?.id)
8587
+ .filter((id) => !!id);
8588
+ const navigations = (await Promise.all(nodeIDs.map((id) => this.navigationService.getNavigationsForNode(id)))).reduce((all, list) => all.concat(list), []);
8589
+ const sweeps = [];
8590
+ for (const navigation of navigations) {
8591
+ if (!navigation?.matterportSweepID || !navigation.position) {
8592
+ continue;
8593
+ }
8594
+ try {
8595
+ const position = JSON.parse(navigation.position);
8596
+ if ([position?.x, position?.y, position?.z].every((value) => typeof value === 'number')) {
8597
+ sweeps.push({
8598
+ sweepID: navigation.matterportSweepID,
8599
+ x: position.x,
8600
+ y: position.y,
8601
+ z: position.z,
8602
+ });
8603
+ }
8604
+ }
8605
+ catch {
8606
+ // One unreadable position should not cost the space all of its sweeps.
8607
+ }
8608
+ }
8609
+ return sweeps;
8610
+ }
8611
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SweepLocatorService, deps: [{ token: CaptureService }, { token: NavigationService }], target: i0.ɵɵFactoryTarget.Injectable });
8612
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SweepLocatorService, providedIn: 'root' });
8613
+ }
8614
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SweepLocatorService, decorators: [{
8615
+ type: Injectable,
8616
+ args: [{
8617
+ providedIn: 'root',
8618
+ }]
8619
+ }], ctorParameters: () => [{ type: CaptureService }, { type: NavigationService }] });
8620
+
8621
+ /* eslint-disable no-await-in-loop */
8622
+ /* eslint-disable camelcase */
8623
+ /* eslint-disable no-underscore-dangle */
8624
+ /* eslint-disable class-methods-use-this */
8625
+ class ViewerService {
8626
+ sdkKey;
8627
+ config;
8628
+ poiService;
7360
8629
  matterportService;
7361
8630
  threeJsService;
8631
+ sweepLocator;
7362
8632
  viewerMode = 'Matterport';
7363
8633
  currentModel3D;
7364
8634
  tourUrl;
@@ -7374,6 +8644,20 @@ class ViewerService {
7374
8644
  * lists that feed tags are exactly that.
7375
8645
  */
7376
8646
  currentSpaceID = null;
8647
+ /**
8648
+ * The Three.js capture on screen, or null in Matterport.
8649
+ *
8650
+ * A space can hold several 3D models. A tag placed in one is recorded with it, so that model
8651
+ * keeps showing the tag whatever selection of tags it is otherwise restricted to.
8652
+ */
8653
+ currentCaptureID = null;
8654
+ /**
8655
+ * Whether the 3D model on screen has been calibrated against the tags' shared space.
8656
+ *
8657
+ * Calibrating is what says the model and that space overlap. Until then a tag placed in the model
8658
+ * is in the model's own coordinates: it belongs to no Matterport sweep and to no other visit.
8659
+ */
8660
+ threeJsCalibrated = false;
7377
8661
  sweepToMove;
7378
8662
  rotationToMove;
7379
8663
  goToTriggered = new Subject();
@@ -7402,12 +8686,13 @@ class ViewerService {
7402
8686
  },
7403
8687
  };
7404
8688
  isObjectLoaded = false;
7405
- constructor(sdkKey, config, poiService, matterportService, threeJsService) {
8689
+ constructor(sdkKey, config, poiService, matterportService, threeJsService, sweepLocator) {
7406
8690
  this.sdkKey = sdkKey;
7407
8691
  this.config = config;
7408
8692
  this.poiService = poiService;
7409
8693
  this.matterportService = matterportService;
7410
8694
  this.threeJsService = threeJsService;
8695
+ this.sweepLocator = sweepLocator;
7411
8696
  this.setMode('Matterport');
7412
8697
  }
7413
8698
  /*-----------------------------------------------------------------------------------------------------------------
@@ -7613,14 +8898,77 @@ class ViewerService {
7613
8898
  if (![position?.x, position?.y, position?.z].every((value) => typeof value === 'number')) {
7614
8899
  return;
7615
8900
  }
8901
+ const origin = getThreeJsPoiOrigin(poi);
7616
8902
  this.threeJsService.upsertTag({
7617
8903
  id: object.id,
7618
8904
  position,
7619
8905
  poiType: poi.type,
7620
8906
  label: object.name ?? object.title ?? null,
7621
8907
  iconUrl: await this.resolveTagIconUrl(poi),
8908
+ originCaptureID: origin?.captureID ?? null,
8909
+ frame: origin?.frame ?? null,
7622
8910
  });
7623
8911
  }
8912
+ /**
8913
+ * Stores the tags the last calibration had to rewrite, and returns how many it could not.
8914
+ *
8915
+ * Calibrating keeps the tags placed in this model where they are by changing their coordinates
8916
+ * rather than their place (see ThreeJsService.applyAlignmentKeepingOwnTags). The scene already
8917
+ * shows the result; this writes it down. Call it once the calibration itself is stored - with the
8918
+ * old calibration still on record, the old coordinates are the ones that match it.
8919
+ */
8920
+ async persistRebasedThreeJsTags() {
8921
+ const rebased = this.threeJsService.takeRebasedTags();
8922
+ let failed = 0;
8923
+ await Promise.all(rebased.map(async (tag) => {
8924
+ try {
8925
+ const poi = await this.poiService.getPoiByElementId(tag.id);
8926
+ if (!poi?.id) {
8927
+ return;
8928
+ }
8929
+ let metadata = {};
8930
+ try {
8931
+ metadata = (poi.metadata ? JSON.parse(poi.metadata) : {}) ?? {};
8932
+ }
8933
+ catch {
8934
+ metadata = {};
8935
+ }
8936
+ // In the shared space from here on, so no longer marked as being in the model's own.
8937
+ delete metadata.frame;
8938
+ if (metadata.normal) {
8939
+ metadata.normal = tag.rotateDirection(metadata.normal);
8940
+ }
8941
+ if (this.currentCaptureID) {
8942
+ metadata.captureID = this.currentCaptureID;
8943
+ }
8944
+ await this.poiService.updatePoi({
8945
+ id: poi.id,
8946
+ coordinate: JSON.stringify(tag.position),
8947
+ metadata: JSON.stringify(metadata),
8948
+ // Its place in the tour may have changed with its coordinates - or it may have one for
8949
+ // the first time, now that the model is calibrated.
8950
+ matterportSweepID: await this.findNearestSweepForThreeJs(tag.position),
8951
+ });
8952
+ }
8953
+ catch (error) {
8954
+ failed += 1;
8955
+ console.warn(`ViewerService: could not store the new coordinates of tag ${tag.id}`, error);
8956
+ }
8957
+ }));
8958
+ return { stored: rebased.length - failed, failed };
8959
+ }
8960
+ /** How many tags the calibration walkthrough could still use: the ones from elsewhere. */
8961
+ getThreeJsCalibrationCandidateCount() {
8962
+ return this.checkMode('threejs') ? this.threeJsService.getCalibrationCandidateCount() : 0;
8963
+ }
8964
+ /** How many tags from elsewhere are not on the 3D model, and so are not shown on it. */
8965
+ getThreeJsOffModelTagCount() {
8966
+ return this.checkMode('threejs') ? this.threeJsService.getOffModelTagCount() : 0;
8967
+ }
8968
+ /** The tags registered in the Three.js scene, shown or not. */
8969
+ getThreeJsTags() {
8970
+ return this.checkMode('threejs') ? this.threeJsService.getTags() : [];
8971
+ }
7624
8972
  /**
7625
8973
  * The image to draw a tag with, mirroring what Matterport shows for the same POI.
7626
8974
  *
@@ -7713,6 +9061,7 @@ class ViewerService {
7713
9061
  if (!alignment) {
7714
9062
  return null;
7715
9063
  }
9064
+ this.threeJsCalibrated = true;
7716
9065
  // Merged into whatever the capture already stores, so an existing camera placement survives.
7717
9066
  let stored = {};
7718
9067
  try {
@@ -7767,7 +9116,11 @@ class ViewerService {
7767
9116
  if (!this.checkMode('threejs')) {
7768
9117
  return;
7769
9118
  }
7770
- this.threeJsService.setTagAlignment(parseThreeJsPlacement(metadata)?.alignment ?? null);
9119
+ const alignment = parseThreeJsPlacement(metadata)?.alignment ?? null;
9120
+ this.threeJsCalibrated = !!alignment;
9121
+ // Which tags were placed in this model is decided against the capture on screen.
9122
+ this.threeJsService.setCurrentCapture(this.currentCaptureID);
9123
+ this.threeJsService.setTagAlignment(alignment);
7771
9124
  }
7772
9125
  /**
7773
9126
  * Enable tag placement menu to add a new element depending on its type (TICKET, DATA, EQUIPMENT etc)
@@ -7836,22 +9189,116 @@ class ViewerService {
7836
9189
  if (this.checkMode('Matterport')) {
7837
9190
  await this.matterportService.action_toolbox_floorplan();
7838
9191
  }
7839
- }
7840
- action_toolbox_mesure() {
7841
- if (this.matterportService.getInteractionMode() !== ViewerInteractions.DEFAULT) {
7842
- this.action_canceled();
9192
+ }
9193
+ action_toolbox_mesure() {
9194
+ if (this.matterportService.getInteractionMode() !== ViewerInteractions.DEFAULT) {
9195
+ this.action_canceled();
9196
+ }
9197
+ this.action_toolbox_inside_view();
9198
+ // Matterport
9199
+ if (this.checkMode('Matterport')) {
9200
+ this.matterportService.action_toolbox_mesure();
9201
+ }
9202
+ // Matterport's SDK brings its own measuring tool; the Three.js viewer has to provide one.
9203
+ if (this.checkMode('threejs')) {
9204
+ this.threeJsService.startMeasuring();
9205
+ }
9206
+ }
9207
+ action_toolbox_cancel_mesure() {
9208
+ // Matterport
9209
+ if (this.checkMode('Matterport')) {
9210
+ this.matterportService.action_toolbox_cancel_mesure();
9211
+ }
9212
+ if (this.checkMode('threejs')) {
9213
+ this.threeJsService.cancelMeasuring();
9214
+ // The toolbar toggles on this flag; left on, the next click would switch measuring "off"
9215
+ // instead of starting a new one.
9216
+ this.toolboxStates.mesure.stat = false;
9217
+ }
9218
+ }
9219
+ /** While measuring in the Three.js viewer: points and segment lengths so far, null if abandoned. */
9220
+ get threeJsMeasurementProgress$() {
9221
+ return this.threeJsService.measurementProgress$;
9222
+ }
9223
+ /** Ends the Three.js measurement in progress, or returns null when it has fewer than two points. */
9224
+ finishThreeJsMeasurement() {
9225
+ if (!this.checkMode('threejs')) {
9226
+ return null;
9227
+ }
9228
+ const measurement = this.threeJsService.finishMeasuring();
9229
+ if (measurement) {
9230
+ this.toolboxStates.mesure.stat = false;
9231
+ }
9232
+ return measurement;
9233
+ }
9234
+ /** The measurement last finished in the Three.js viewer, for the form that saves it. */
9235
+ getThreeJsLastMeasurement() {
9236
+ return this.checkMode('threejs') ? this.threeJsService.getLastMeasurement() : null;
9237
+ }
9238
+ /**
9239
+ * The Matterport sweep nearest to a point picked in the Three.js viewer, or null.
9240
+ *
9241
+ * For callers that write their own POI, as the measurement form does - anything created through
9242
+ * createPoiForThreeJsPlacement already gets this.
9243
+ */
9244
+ findNearestSweepForThreeJs(point) {
9245
+ // An uncalibrated model's coordinates are its own. Measured against the Matterport sweeps they
9246
+ // could land near one by coincidence, and the tag would turn up in the tour somewhere arbitrary.
9247
+ if (!this.threeJsCalibrated) {
9248
+ return Promise.resolve(null);
9249
+ }
9250
+ return this.sweepLocator.findNearestSweep(point, this.currentSpaceID);
9251
+ }
9252
+ /**
9253
+ * The metadata a POI placed in the Three.js viewer is stored with: which 3D model it was placed
9254
+ * in, and - when that model is not calibrated - that its coordinates are the model's own.
9255
+ */
9256
+ buildThreeJsPoiMetadata(extra = {}) {
9257
+ return JSON.stringify({
9258
+ ...extra,
9259
+ ...(this.currentCaptureID ? { captureID: this.currentCaptureID } : {}),
9260
+ ...(this.currentCaptureID && !this.threeJsCalibrated ? { frame: 'model' } : {}),
9261
+ });
9262
+ }
9263
+ /** Which of the 3D model's axes is being treated as up. */
9264
+ getThreeJsModelUp() {
9265
+ return this.threeJsService.getModelUp();
9266
+ }
9267
+ /**
9268
+ * Turns the 3D model to the next candidate for "which axis is up", and returns the capture
9269
+ * metadata with that choice written in, for the caller to store.
9270
+ *
9271
+ * The vertical axis is worked out from the geometry when a model loads, and that can be wrong:
9272
+ * a narrow corridor and a low slab are the same shape turned over. This is the way to correct it.
9273
+ */
9274
+ cycleThreeJsModelUp(existingMetadata) {
9275
+ if (!this.checkMode('threejs')) {
9276
+ return null;
9277
+ }
9278
+ const current = THREEJS_MODEL_UP_OPTIONS.indexOf(this.threeJsService.getModelUp());
9279
+ const up = THREEJS_MODEL_UP_OPTIONS[(current + 1) % THREEJS_MODEL_UP_OPTIONS.length];
9280
+ this.threeJsService.setModelUp(up);
9281
+ let stored = {};
9282
+ try {
9283
+ stored = existingMetadata ? JSON.parse(existingMetadata) : {};
7843
9284
  }
7844
- this.action_toolbox_inside_view();
7845
- // Matterport
7846
- if (this.checkMode('Matterport')) {
7847
- this.matterportService.action_toolbox_mesure();
9285
+ catch {
9286
+ stored = {};
7848
9287
  }
9288
+ stored.up = up;
9289
+ return { up, metadata: JSON.stringify(stored) };
7849
9290
  }
7850
- action_toolbox_cancel_mesure() {
7851
- // Matterport
7852
- if (this.checkMode('Matterport')) {
7853
- this.matterportService.action_toolbox_cancel_mesure();
7854
- }
9291
+ /** Chooses the tag for the calibration pair being matched by id, instead of by clicking it. */
9292
+ selectThreeJsAlignmentTag(elementId) {
9293
+ return this.checkMode('threejs') && this.threeJsService.selectAlignmentTag(elementId);
9294
+ }
9295
+ /** How far the visible tags sit from the 3D model, in model radii. Null without both. */
9296
+ getThreeJsTagDistanceFromModel() {
9297
+ return this.checkMode('threejs') ? this.threeJsService.getTagDistanceFromModel() : null;
9298
+ }
9299
+ /** How many tags the Three.js scene is showing. */
9300
+ getThreeJsTagCount() {
9301
+ return this.checkMode('threejs') ? this.threeJsService.getTagCount() : 0;
7855
9302
  }
7856
9303
  async action_update_poi(poi, isValid, element) {
7857
9304
  if (this.checkMode('Matterport')) {
@@ -7941,11 +9388,15 @@ class ViewerService {
7941
9388
  if (!placement || !element?.id) {
7942
9389
  return null;
7943
9390
  }
9391
+ // Without a sweep the tag is left off everything on the Matterport side that is organised by
9392
+ // sweep - the floor plan most visibly, which only draws tags whose sweep belongs to it.
9393
+ const matterportSweepID = await this.findNearestSweepForThreeJs(placement.position);
7944
9394
  const poi = await this.poiService.createPOIForSpaceID({
7945
9395
  coordinate: JSON.stringify(placement.position),
7946
9396
  type: poiType,
7947
9397
  elementID: element.id,
7948
- metadata: JSON.stringify({ normal: placement.normal }),
9398
+ metadata: this.buildThreeJsPoiMetadata({ normal: placement.normal }),
9399
+ ...(matterportSweepID ? { matterportSweepID } : {}),
7949
9400
  });
7950
9401
  // Draw it straight away rather than waiting for the next refresh, so the user sees where the
7951
9402
  // thing they just created ended up. Through the same path the refresh uses, so it arrives with
@@ -8075,7 +9526,7 @@ class ViewerService {
8075
9526
  const placement = parseThreeJsPlacement(placementMetadata);
8076
9527
  // The key, not the signed URL, is what identifies the model across loads - the signature makes
8077
9528
  // every URL unique, which is exactly why the browser's own cache never helps here.
8078
- this.threeJsService.loadGLTFModel(url, placement?.cameraPosition, placement?.rotation, key);
9529
+ this.threeJsService.loadGLTFModel(url, placement?.cameraPosition, placement?.rotation, key, placement?.up);
8079
9530
  }
8080
9531
  /**
8081
9532
  * Loads a 3D model into the Three.js scene (OBJ).
@@ -8170,7 +9621,7 @@ class ViewerService {
8170
9621
  isObject3DLoadedInPosition() {
8171
9622
  return this.isObjectLoaded;
8172
9623
  }
8173
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, deps: [{ token: MATTERPORT_SDK_KEY }, { token: 'config' }, { token: PoiService }, { token: MatterportService }, { token: ThreeJsService }], target: i0.ɵɵFactoryTarget.Injectable });
9624
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, deps: [{ token: MATTERPORT_SDK_KEY }, { token: 'config' }, { token: PoiService }, { token: MatterportService }, { token: ThreeJsService }, { token: SweepLocatorService }], target: i0.ɵɵFactoryTarget.Injectable });
8174
9625
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, providedIn: 'root' });
8175
9626
  }
8176
9627
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, decorators: [{
@@ -8184,7 +9635,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
8184
9635
  }] }, { type: Config, decorators: [{
8185
9636
  type: Inject,
8186
9637
  args: ['config']
8187
- }] }, { type: PoiService }, { type: MatterportService }, { type: ThreeJsService }] });
9638
+ }] }, { type: PoiService }, { type: MatterportService }, { type: ThreeJsService }, { type: SweepLocatorService }] });
8188
9639
 
8189
9640
  class ModalSwitchVisitComponent {
8190
9641
  activeModal;
@@ -8212,11 +9663,11 @@ class ModalSwitchVisitComponent {
8212
9663
  });
8213
9664
  }
8214
9665
  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 });
8215
- 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" }] });
9666
+ 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" }] });
8216
9667
  }
8217
9668
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ModalSwitchVisitComponent, decorators: [{
8218
9669
  type: Component,
8219
- 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"] }]
9670
+ 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"] }]
8220
9671
  }], ctorParameters: () => [{ type: i1$2.NgbActiveModal }, { type: i1$1.Router }, { type: ViewerService }], propDecorators: { content: [{
8221
9672
  type: Input
8222
9673
  }], model3D: [{
@@ -8227,6 +9678,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
8227
9678
  type: Input
8228
9679
  }] } });
8229
9680
 
9681
+ /**
9682
+ * Where a POI placed in a Three.js model says it was placed, or null for any other POI.
9683
+ *
9684
+ * `frame` is 'model' when its coordinates are in that model's own space - it was placed before the
9685
+ * model was calibrated - rather than in the space tags share with the other visits.
9686
+ */
9687
+ function getThreeJsPoiOrigin(poi) {
9688
+ if (!poi?.metadata) {
9689
+ return null;
9690
+ }
9691
+ try {
9692
+ const parsed = JSON.parse(poi.metadata);
9693
+ return typeof parsed?.captureID === "string"
9694
+ ? { captureID: parsed.captureID, frame: parsed.frame === "model" ? "model" : null }
9695
+ : null;
9696
+ }
9697
+ catch {
9698
+ return null;
9699
+ }
9700
+ }
9701
+ const THREEJS_MODEL_UP_OPTIONS = ["y", "z", "x", "-y", "-z", "-x"];
8230
9702
  /**
8231
9703
  * Parses the placement stored in a Three.js capture's metadata, or null when there is none.
8232
9704
  *
@@ -8265,7 +9737,12 @@ function parseThreeJsPlacement(metadata) {
8265
9737
  placement.alignment = alignment;
8266
9738
  }
8267
9739
  }
8268
- return placement.cameraPosition || placement.rotation || placement.alignment ? placement : null;
9740
+ if (THREEJS_MODEL_UP_OPTIONS.includes(parsed.up)) {
9741
+ placement.up = parsed.up;
9742
+ }
9743
+ return placement.cameraPosition || placement.rotation || placement.alignment || placement.up
9744
+ ? placement
9745
+ : null;
8269
9746
  }
8270
9747
  catch {
8271
9748
  console.warn("Ignoring unparseable Three.js placement metadata:", metadata);
@@ -8690,285 +10167,57 @@ function shuffleArray(array) {
8690
10167
  const newArray = [...array];
8691
10168
  for (let index = array.length - 1; index > 0; index--) {
8692
10169
  const indexNew = Math.floor(Math.random() * (index + 1));
8693
- // eslint-disable-next-line no-param-reassign
8694
- [newArray[index], newArray[indexNew]] = [
8695
- newArray[indexNew],
8696
- newArray[index],
8697
- ];
8698
- }
8699
- return newArray;
8700
- }
8701
- function numberToDateString(timestamp) {
8702
- const jsDate = new Date(timestamp);
8703
- return jsDate.toLocaleString("fr-FR");
8704
- }
8705
- function durationToString(duration, translate) {
8706
- const days = Math.floor(duration / 3600 / 24);
8707
- const hours = Math.floor((duration - days * 24 * 3600) / 3600);
8708
- const minutes = Math.floor((duration % 3600) / 60);
8709
- const seconds = Math.floor(duration % 60);
8710
- const daysString = translate.instant("day(s)");
8711
- const hoursString = translate.instant("hour(s)");
8712
- const oneHourString = translate.instant("hour");
8713
- const minutesString = translate.instant("minute(s)");
8714
- const secondsString = translate.instant("second(s)");
8715
- if (duration > 3600 * 24 * 7) {
8716
- return `${days} ${daysString}`;
8717
- }
8718
- if (duration > 3600 * 24) {
8719
- return `${days} ${daysString} ${hours} ${hoursString}`;
8720
- }
8721
- if (duration === 3600) {
8722
- return `${hours} ${oneHourString}`;
8723
- }
8724
- if (duration > 3600) {
8725
- if (minutes === 0) {
8726
- return `${hours} ${hoursString}`;
8727
- }
8728
- return `${hours} ${hoursString} ${minutes} ${minutesString}`;
8729
- }
8730
- if (duration >= 60) {
8731
- if (seconds === 0) {
8732
- return `${minutes} ${minutesString}`;
8733
- }
8734
- return `${minutes} ${minutesString} ${seconds} ${secondsString}`;
8735
- }
8736
- return `${seconds} ${secondsString}`;
8737
- }
8738
- function pointToString(point) {
8739
- const x = point.x.toFixed(3);
8740
- const y = point.y.toFixed(3);
8741
- const z = point.z.toFixed(3);
8742
- return `{ x: ${x}, y: ${y}, z: ${z} }`;
8743
- }
8744
-
8745
- /* eslint-disable class-methods-use-this */
8746
- class ZoneService {
8747
- userService;
8748
- API; //AWS APIService
8749
- constructor(apiInjected, userService) {
8750
- this.userService = userService;
8751
- this.API = apiInjected;
8752
- }
8753
- async getZonesBySpace(spaceID) {
8754
- const list = [];
8755
- if (this.API.__proto__.hasOwnProperty('ZonesBySpace')) {
8756
- // Paginated: without a nextToken loop, only the first page (100 zones) was returned and
8757
- // the sweeps of the other zones silently disappeared.
8758
- let nextToken = null;
8759
- do {
8760
- const zones = await this.API.__proto__.ZonesBySpace(spaceID, null, null, 1000, nextToken);
8761
- for (const zone of zones?.items || []) {
8762
- const zoneObject = { ...zone };
8763
- zoneObject.virtual = this.zoneIsVirtual(zone);
8764
- list.push(zoneObject);
8765
- }
8766
- nextToken = zones?.nextToken ?? null;
8767
- } while (nextToken);
8768
- return list;
8769
- }
8770
- else {
8771
- throw new Error('ZonesBySpace not implemented');
8772
- }
8773
- }
8774
- async getZonesBySpaceForUser(spaceID) {
8775
- const missionForLocation = this.userService.currentMission(spaceID);
8776
- const list = [];
8777
- if (missionForLocation && missionForLocation.zoneID) {
8778
- const allowedZone = await this.getZone(missionForLocation.zoneID);
8779
- allowedZone.virtual = this.zoneIsVirtual(allowedZone);
8780
- if (!allowedZone.parent) {
8781
- // the whole building is authorized
8782
- return this.getZonesBySpace(spaceID);
8783
- }
8784
- list.push(allowedZone);
8785
- // allow all children zones of an allowed parent
8786
- const children = (await this.API.__proto__.ZonesByZone(allowedZone.id))
8787
- .items;
8788
- if (children.length > 0) {
8789
- for (const child of children) {
8790
- const childZone = { ...child };
8791
- childZone.virtual = this.zoneIsVirtual(child);
8792
- list.push(childZone);
8793
- }
8794
- }
8795
- // allow the parent Floor of the allowed zone
8796
- if (allowedZone.parent.layer &&
8797
- allowedZone.parent.layer.name === 'FLOOR') {
8798
- list.push(allowedZone.parent);
8799
- }
8800
- }
8801
- else {
8802
- return this.getZonesBySpace(spaceID);
8803
- }
8804
- return list;
8805
- }
8806
- async filterZonesIDsForUser(zones) {
8807
- const { spaceID } = zones[0];
8808
- const zoneIDs = new Set(zones.map((zone) => zone.id));
8809
- const allowedZones = await this.getZonesBySpaceForUser(spaceID);
8810
- const filteredZones = allowedZones.filter((zone) => zoneIDs.has(zone.id));
8811
- return filteredZones;
8812
- }
8813
- async deleteZone(zoneID) {
8814
- return this.API.__proto__.DeleteZone({ id: zoneID });
8815
- }
8816
- async getZonesForSweepForSpace(sweepID, spaceID) {
8817
- const zonesSpace = (await this.API.__proto__.ZonesBySpace(spaceID)).items;
8818
- return zonesSpace.filter((zone) => zone.sweepIDs && zone.sweepIDs.includes(sweepID));
8819
- }
8820
- // // eslint-disable-next-line class-methods-use-this
8821
- sortZonesFromChildToParent(zonesToSort) {
8822
- // get default
8823
- const defaultZone = zonesToSort.find((z) => !z.parentID);
8824
- if (defaultZone) {
8825
- const indexOf = zonesToSort.indexOf(defaultZone);
8826
- zonesToSort.splice(indexOf, 1);
8827
- }
8828
- const zones = zonesToSort.sort((a, b) => {
8829
- if (a.parentID === b.id) {
8830
- return -1;
8831
- }
8832
- return 0;
8833
- });
8834
- // so far by number of navigation points in them
8835
- // const zones = zonesToSort.sort(
8836
- // (a, b) => a.navigations.items.length - b.navigations.items.length,
8837
- // );
8838
- if (defaultZone) {
8839
- zones.push(defaultZone);
8840
- }
8841
- return zones;
8842
- }
8843
- async getZonesForObject(object) {
8844
- if (object.pois.items && object.pois.items.length > 0) {
8845
- const [poi] = object.pois.items;
8846
- if (poi.matterportSweepID) {
8847
- const zones = await this.getZonesForSweepForSpace(poi.matterportSweepID, object.spaceID);
8848
- return zones;
8849
- }
8850
- }
8851
- return null;
8852
- }
8853
- async createVirtualZone(zoneInput) {
8854
- zoneInput.sweepIDs = [];
8855
- const zone = await this.API.__proto__.CreateZone(zoneInput);
8856
- console.log('virtual zone created', zone);
8857
- return zone;
8858
- }
8859
- zoneIsVirtual(zone) {
8860
- if (!zone.sweepIDs) {
8861
- return true;
8862
- }
8863
- if ((zone.tags && zone.tags.includes('virtual')) ||
8864
- zone.sweepIDs.length === 0) {
8865
- return true;
8866
- }
8867
- return false;
8868
- }
8869
- async getModel3DForZone(zoneID) {
8870
- const zone = await this.API.__proto__.GetZone(zoneID);
8871
- if (zone.nodeIDs) {
8872
- const node = await this.API.__proto__.GetNode(zone.nodeIDs[0]);
8873
- return node.model3D;
8874
- }
8875
- return null;
8876
- }
8877
- async getZone(zoneID) {
8878
- return this.API.__proto__.GetZone(zoneID);
8879
- }
8880
- async getDefaultZoneForSpace(spaceID) {
8881
- return this.getZonesBySpace(spaceID).then(async (zones) => {
8882
- const zonesBuilding = zones.filter((zone) => zone.layer && zone.layer.name === 'BUILDING');
8883
- if (zonesBuilding.length === 1) {
8884
- return zonesBuilding[0];
8885
- }
8886
- const parentZone = zones.find((zone) => !zone.parentID);
8887
- return parentZone;
8888
- });
8889
- }
8890
- async create(input) {
8891
- return this.API.__proto__.CreateZone(input);
8892
- }
8893
- // async getSortedZonesForSweepForUser(
8894
- // sweep: string,
8895
- // spaceID: string,
8896
- // ): Promise<Zone[]> {
8897
- // if (!spaceID) {
8898
- // return [];
8899
- // }
8900
- // const zones = await this.getZonesForSweepForSpace(sweep, spaceID);
8901
- // if (zones.length === 0) {
8902
- // return [];
8903
- // }
8904
- // const filteredZones = await this.filterZonesIDsForUser(zones);
8905
- // if (filteredZones.length > 0) {
8906
- // let zonesSorted = this.sortZonesFromChildToParent(filteredZones);
8907
- // zonesSorted = zonesSorted.filter((zone) => zone !== undefined);
8908
- // return zonesSorted;
8909
- // }
8910
- // return [];
8911
- // }
8912
- async updateZone(input) {
8913
- return this.API.__proto__.UpdateZone(input);
10170
+ // eslint-disable-next-line no-param-reassign
10171
+ [newArray[index], newArray[indexNew]] = [
10172
+ newArray[indexNew],
10173
+ newArray[index],
10174
+ ];
8914
10175
  }
8915
- async getZonesForNodeForSpace(nodeID, spaceID) {
8916
- return this.API.__proto__.ZonesBySpace(spaceID).then(async (zones) => {
8917
- const filteredZones = zones.items.filter((zone) => zone.nodeIDs && zone.nodeIDs.includes(nodeID));
8918
- return filteredZones;
8919
- });
10176
+ return newArray;
10177
+ }
10178
+ function numberToDateString(timestamp) {
10179
+ const jsDate = new Date(timestamp);
10180
+ return jsDate.toLocaleString("fr-FR");
10181
+ }
10182
+ function durationToString(duration, translate) {
10183
+ const days = Math.floor(duration / 3600 / 24);
10184
+ const hours = Math.floor((duration - days * 24 * 3600) / 3600);
10185
+ const minutes = Math.floor((duration % 3600) / 60);
10186
+ const seconds = Math.floor(duration % 60);
10187
+ const daysString = translate.instant("day(s)");
10188
+ const hoursString = translate.instant("hour(s)");
10189
+ const oneHourString = translate.instant("hour");
10190
+ const minutesString = translate.instant("minute(s)");
10191
+ const secondsString = translate.instant("second(s)");
10192
+ if (duration > 3600 * 24 * 7) {
10193
+ return `${days} ${daysString}`;
8920
10194
  }
8921
- /**
8922
- * Gets the Zone (layer FLOOR) for a matterport sweep ID in a given Space
8923
- * @param sweepID
8924
- * @param spaceID
8925
- * @returns Zone
8926
- */
8927
- async getFloorZoneForSweepInSpace(sweepID, spaceID) {
8928
- const zones = await this.getZonesForSweepForSpace(sweepID, spaceID);
8929
- const floors = zones.filter((zone) => zone.layer && zone.layer.name.includes('FLOOR'));
8930
- return floors.length > 0 ? floors[0] : null;
10195
+ if (duration > 3600 * 24) {
10196
+ return `${days} ${daysString} ${hours} ${hoursString}`;
8931
10197
  }
8932
- /**
8933
- * Get zones with audio track for entire space
8934
- * @param spaceID
8935
- * @returns Promise<Zone[]>
8936
- */
8937
- async getAudioZonesForSpace(spaceID) {
8938
- const allZones = await this.getZonesBySpace(spaceID);
8939
- return allZones.filter((zone) => zone.audioID);
10198
+ if (duration === 3600) {
10199
+ return `${hours} ${oneHourString}`;
8940
10200
  }
8941
- /**
8942
- * Get audio zones for user (within allowed zones)
8943
- * @param spaceID
8944
- * @returns Promise<Zone[]>
8945
- */
8946
- async getAudioZonesForUserForSpace(spaceID) {
8947
- const userZones = await this.getZonesBySpaceForUser(spaceID);
8948
- return userZones.filter((zone) => zone.audioID);
10201
+ if (duration > 3600) {
10202
+ if (minutes === 0) {
10203
+ return `${hours} ${hoursString}`;
10204
+ }
10205
+ return `${hours} ${hoursString} ${minutes} ${minutesString}`;
8949
10206
  }
8950
- /**
8951
- * Get Zones by Parent Zone ID
8952
- * @param parentZoneID string
8953
- * @returns Promise<Zone[]>
8954
- */
8955
- async getZonesByParentZone(parentZoneID) {
8956
- return this.API.__proto__
8957
- .ZonesByZone(parentZoneID)
8958
- .then((res) => res.items);
10207
+ if (duration >= 60) {
10208
+ if (seconds === 0) {
10209
+ return `${minutes} ${minutesString}`;
10210
+ }
10211
+ return `${minutes} ${minutesString} ${seconds} ${secondsString}`;
8959
10212
  }
8960
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ZoneService, deps: [{ token: 'currentAPIService' }, { token: BaseUserService }], target: i0.ɵɵFactoryTarget.Injectable });
8961
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ZoneService, providedIn: 'root' });
10213
+ return `${seconds} ${secondsString}`;
10214
+ }
10215
+ function pointToString(point) {
10216
+ const x = point.x.toFixed(3);
10217
+ const y = point.y.toFixed(3);
10218
+ const z = point.z.toFixed(3);
10219
+ return `{ x: ${x}, y: ${y}, z: ${z} }`;
8962
10220
  }
8963
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ZoneService, decorators: [{
8964
- type: Injectable,
8965
- args: [{
8966
- providedIn: 'root',
8967
- }]
8968
- }], ctorParameters: () => [{ type: undefined, decorators: [{
8969
- type: Inject,
8970
- args: ['currentAPIService']
8971
- }] }, { type: BaseUserService }] });
8972
10221
 
8973
10222
  /* eslint-disable no-case-declarations */
8974
10223
  /* eslint-disable no-await-in-loop */
@@ -9186,7 +10435,12 @@ class FilterService {
9186
10435
  // visibility filter already reads "no sweep" as "not floor-scoped, show it";
9187
10436
  // this is the same rule applied to zones.
9188
10437
  if (poi && poi.coordinate && !poi.matterportSweepID) {
9189
- filteredObjects.push(object);
10438
+ // ...unless it was placed in a 3D model. With no sweep, that model was never
10439
+ // tied to this tour: the coordinates are the model's own, and here they would
10440
+ // put the tag somewhere arbitrary.
10441
+ if (!getThreeJsPoiOrigin(poi)) {
10442
+ filteredObjects.push(object);
10443
+ }
9190
10444
  return;
9191
10445
  }
9192
10446
  if (poi && zone) {
@@ -9581,133 +10835,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
9581
10835
  }]
9582
10836
  }], ctorParameters: () => [{ type: BaseUserService }, { type: ZoneService }, { type: PoiService }] });
9583
10837
 
9584
- class NavigationService {
9585
- userService;
9586
- zoneService;
9587
- API;
9588
- constructor(apiInjected, userService, zoneService) {
9589
- this.userService = userService;
9590
- this.zoneService = zoneService;
9591
- this.API = apiInjected;
9592
- }
9593
- async createNavigation(navInput) {
9594
- return this.API.__proto__.CreateNavigation(navInput);
9595
- }
9596
- async deleteNavigation(id) {
9597
- return this.API.__proto__.DeleteNavigation({ id });
9598
- }
9599
- async getNavigationsForNode(nodeID) {
9600
- const limit = 200;
9601
- let navigationsTokenDict = { navigations: [], nextToken: null };
9602
- do {
9603
- navigationsTokenDict =
9604
- await this.mergeAPIResponseWithNextTokenWithLimit(nodeID, limit, navigationsTokenDict);
9605
- } while (navigationsTokenDict.nextToken);
9606
- return navigationsTokenDict.navigations;
9607
- }
9608
- async mergeAPIResponseWithNextTokenWithLimit(nodeID, limit, navigationsTokenDict) {
9609
- return this.API.__proto__.NavigationsByNode(nodeID, null, null, limit, navigationsTokenDict.nextToken).then((response) => {
9610
- const navigations = response.items;
9611
- const navObject = {
9612
- navigations: [...navigationsTokenDict.navigations, ...navigations],
9613
- nextToken: response.nextToken,
9614
- };
9615
- return navObject;
9616
- });
9617
- }
9618
- async getNavigation(id) {
9619
- return this.API.__proto__.GetNavigation(id);
9620
- }
9621
- async getSweepsIDsForSpace(spaceID) {
9622
- const zones = await this.zoneService.getZonesBySpace(spaceID);
9623
- const zonesWithSweeps = zones.filter((zone) => zone.sweepIDs && zone.sweepIDs.length > 0);
9624
- const allSweeps = zonesWithSweeps.flatMap((zone) => zone.sweepIDs);
9625
- return [...new Set(allSweeps)];
9626
- }
9627
- async getAllowedSweepsIDsForUserInSpace(spaceID) {
9628
- const missionsForSpace = this.userService.currentMissions.filter((mis) => mis.spaceID === spaceID);
9629
- console.log(missionsForSpace);
9630
- let navIDs = [];
9631
- if (this.userService.userHasAccessToAllZonesInSpace(spaceID)) {
9632
- console.log(1);
9633
- return this.getSweepsIDsForSpace(spaceID);
9634
- }
9635
- await Promise.all(missionsForSpace.map(async (mission) => {
9636
- navIDs = [...navIDs, ...mission.zone.sweepIDs];
9637
- }));
9638
- console.log('final navids = ', navIDs);
9639
- return navIDs;
9640
- }
9641
- async getForbiddenNavigationIDsForUserInSpace(spaceID) {
9642
- const allowed = await this.getAllowedSweepsIDsForUserInSpace(spaceID);
9643
- const allNavs = await this.getSweepsIDsForSpace(spaceID);
9644
- const difference = allNavs.filter((x) => !allowed.includes(x));
9645
- return difference.length > 0 ? difference : null;
9646
- }
9647
- async getNavigationsForMatterportIDForSpace(matterportID, spaceID) {
9648
- return this.API.__proto__.NavigationsByMatterportIDBySpace(matterportID, {
9649
- eq: spaceID,
9650
- }).then((navs) => navs.items);
9651
- }
9652
- async getNavigationsForZone(zone) {
9653
- const responseNavs = [];
9654
- await Promise.all(zone.sweepIDs.map(async (sweepID) => {
9655
- const navs = await this.getNavigationsForMatterportIDForSpace(sweepID, zone.spaceID);
9656
- if (navs.length > 0) {
9657
- responseNavs.push(navs[0]);
9658
- }
9659
- }));
9660
- return responseNavs;
9661
- }
9662
- async updateNavigation(navInput) {
9663
- return this.API.__proto__.UpdateNavigation(navInput);
9664
- }
9665
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NavigationService, deps: [{ token: 'currentAPIService' }, { token: BaseUserService }, { token: ZoneService }], target: i0.ɵɵFactoryTarget.Injectable });
9666
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NavigationService, providedIn: "root" });
9667
- }
9668
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NavigationService, decorators: [{
9669
- type: Injectable,
9670
- args: [{
9671
- providedIn: "root",
9672
- }]
9673
- }], ctorParameters: () => [{ type: undefined, decorators: [{
9674
- type: Inject,
9675
- args: ['currentAPIService']
9676
- }] }, { type: BaseUserService }, { type: ZoneService }] });
9677
-
9678
- class CaptureService {
9679
- API; //AWS
9680
- constructor(apiInjected) {
9681
- this.API = apiInjected;
9682
- }
9683
- async getCapturesBySpace(spaceID) {
9684
- return this.API.CapturesBySpace(spaceID).then((cap) => cap.items);
9685
- }
9686
- async getCapture(captureID) {
9687
- return this.API.__proto__.GetCapture(captureID);
9688
- }
9689
- async deleteCapture(captureID) {
9690
- return this.API.__proto__.DeleteCapture({ id: captureID });
9691
- }
9692
- async updateCapture(capture) {
9693
- return this.API.__proto__.UpdateCapture(capture);
9694
- }
9695
- async createCapture(capture) {
9696
- return this.API.__proto__.CreateCapture(capture);
9697
- }
9698
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CaptureService, deps: [{ token: 'currentAPIService' }], target: i0.ɵɵFactoryTarget.Injectable });
9699
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CaptureService, providedIn: "root" });
9700
- }
9701
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CaptureService, decorators: [{
9702
- type: Injectable,
9703
- args: [{
9704
- providedIn: "root",
9705
- }]
9706
- }], ctorParameters: () => [{ type: undefined, decorators: [{
9707
- type: Inject,
9708
- args: ['currentAPIService']
9709
- }] }] });
9710
-
9711
10838
  class NodeService {
9712
10839
  API;
9713
10840
  constructor(apiInjected) {
@@ -11043,7 +12170,7 @@ class BaseObjectService {
11043
12170
  // is no sweep to resolve a visit from, which is why this used to report the model
11044
12171
  // as unreachable. The space's 3D visit is the one that can show it.
11045
12172
  if (poi && poi.coordinate && !poi.matterportSweepID) {
11046
- const visit = await this.getThreeJsVisitForSpace(object.spaceID);
12173
+ const visit = await this.getThreeJsVisitForSpace(object.spaceID, getThreeJsPoiOrigin(poi)?.captureID);
11047
12174
  return visit
11048
12175
  ? { model3D: visit.model3d, sweep: null, rotation: null }
11049
12176
  : null;
@@ -11079,15 +12206,15 @@ class BaseObjectService {
11079
12206
  });
11080
12207
  }
11081
12208
  /**
11082
- * The space's 3D visit, or null when it has none.
12209
+ * The 3D visit to show a POI in, or null when the space has none.
11083
12210
  *
11084
- * Takes the first one the database returns. That is not the space's chosen default - the
11085
- * default ordering lives in SpaceService, which cannot be injected here. The POI does not record
11086
- * which capture it was placed in, so with several 3D models this is a guess either way.
12211
+ * The one the POI was placed in, when it says. Older POIs do not, and for those it is the first
12212
+ * 3D visit the database returns - a guess, when the space has several.
11087
12213
  */
11088
- async getThreeJsVisitForSpace(spaceID) {
11089
- const visits = await this.visitService.getVisitsForSpaceForUser(spaceID, false);
11090
- return (visits ?? []).find((visit) => visit.viewer === CaptureViewer.THREEJS) ?? null;
12214
+ async getThreeJsVisitForSpace(spaceID, captureID) {
12215
+ const visits = (await this.visitService.getVisitsForSpaceForUser(spaceID, false) ?? [])
12216
+ .filter((visit) => visit.viewer === CaptureViewer.THREEJS);
12217
+ return visits.find((visit) => visit.captureID === captureID) ?? visits[0] ?? null;
11091
12218
  }
11092
12219
  async updateIconForAllObjects(objects, tagIcon) {
11093
12220
  const IDs = [];
@@ -12483,7 +13610,7 @@ class EquipmentService extends BaseObjectService {
12483
13610
  user: this.userService.cu,
12484
13611
  locationId: this.currentSpaceID,
12485
13612
  timestamp: new Date(),
12486
- zoneId: this.currentZone.id,
13613
+ zoneId: this.currentZone?.id,
12487
13614
  floorId: this.selectedFloor?.id,
12488
13615
  },
12489
13616
  streamName: 'smarterplanKinesis-analytics',
@@ -12947,6 +14074,11 @@ class EquipmentService extends BaseObjectService {
12947
14074
  }
12948
14075
  updateDueToCreate(createdEquip) {
12949
14076
  this.currentEquipments.space.unshift(createdEquip);
14077
+ // initEquips only records a zone's list when it has a zone or found equipments, so the first
14078
+ // equipment of a space viewed without zones (Three.js) arrives with no list to join.
14079
+ if (!this.currentEquipments.zonesMap.has(this.zoneIDFilter)) {
14080
+ this.currentEquipments.zonesMap.set(this.zoneIDFilter, []);
14081
+ }
12950
14082
  this.currentEquipments.zonesMap
12951
14083
  .get(this.zoneIDFilter)
12952
14084
  .unshift(createdEquip);
@@ -13020,6 +14152,26 @@ class EquipmentService extends BaseObjectService {
13020
14152
  }
13021
14153
  }
13022
14154
  });
14155
+ // The Three.js viewer shows a whole space at once, with no zone to navigate into, so it
14156
+ // announces itself here instead of through `zoneChange`. Leaving the zone filter empty is what
14157
+ // makes every equipment of the space show up.
14158
+ this.zoneChangeService.spaceWithoutZones
14159
+ .pipe(takeUntil(this.destroy$))
14160
+ .subscribe((spaceID) => {
14161
+ this.currentSpaceID = spaceID;
14162
+ this.zoneIDFilter = null;
14163
+ this.currentZone = null;
14164
+ this.currentEquipments = {
14165
+ space: [],
14166
+ zonesMap: new Map(),
14167
+ };
14168
+ if (!spaceID) {
14169
+ this.equipmentsTags.next(null);
14170
+ this.equipmentsUpdated.next({ space: [], zone: null });
14171
+ return;
14172
+ }
14173
+ this.initEquips();
14174
+ });
13023
14175
  this.deleteObservable
13024
14176
  .pipe(takeUntil(this.destroy$))
13025
14177
  .subscribe((equipment) => {
@@ -13234,11 +14386,11 @@ class SearchBarComponent {
13234
14386
  this.searchEvent.emit(results);
13235
14387
  }
13236
14388
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SearchBarComponent, deps: [{ token: SearchService }], target: i0.ɵɵFactoryTarget.Component });
13237
- 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" }] });
14389
+ 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" }] });
13238
14390
  }
13239
14391
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SearchBarComponent, decorators: [{
13240
14392
  type: Component,
13241
- 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"] }]
14393
+ 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"] }]
13242
14394
  }], ctorParameters: () => [{ type: SearchService }], propDecorators: { searchable: [{
13243
14395
  type: Input
13244
14396
  }], objectType: [{
@@ -13686,11 +14838,11 @@ class LoaderComponent {
13686
14838
  document.documentElement.style.setProperty('--color', this.color);
13687
14839
  }
13688
14840
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LoaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13689
- 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"] }] });
14841
+ 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"] }] });
13690
14842
  }
13691
14843
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LoaderComponent, decorators: [{
13692
14844
  type: Component,
13693
- 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"] }]
14845
+ 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"] }]
13694
14846
  }], ctorParameters: () => [], propDecorators: { useLogo: [{
13695
14847
  type: Input
13696
14848
  }], color: [{
@@ -13748,11 +14900,11 @@ class SupportModalComponent {
13748
14900
  }
13749
14901
  }
13750
14902
  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 });
13751
- 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" }] });
14903
+ 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" }] });
13752
14904
  }
13753
14905
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SupportModalComponent, decorators: [{
13754
14906
  type: Component,
13755
- 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"] }]
14907
+ 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"] }]
13756
14908
  }], ctorParameters: () => [{ type: SupportService }, { type: i1$2.NgbActiveModal }, { type: i3.UntypedFormBuilder }, { type: BaseUserService }, { type: ValidatorsService }] });
13757
14909
 
13758
14910
  class LocaleService {
@@ -13994,6 +15146,19 @@ class FeatureService extends BaseObjectService {
13994
15146
  this.initFeatures();
13995
15147
  }
13996
15148
  });
15149
+ // The Three.js viewer shows a whole space at once, with no zone to navigate into, so it
15150
+ // announces itself here instead of through `zoneChange`.
15151
+ this.zoneChangeService.spaceWithoutZones.subscribe((spaceID) => {
15152
+ this.currentSpaceID = spaceID;
15153
+ this.zoneIDFilter = null;
15154
+ if (!spaceID) {
15155
+ this.featureTags.next(null);
15156
+ this.featureUpdated.next({ space: [], zone: null });
15157
+ }
15158
+ else if (!this.userService.isMuseumUser(spaceID)) {
15159
+ this.initFeatures();
15160
+ }
15161
+ });
13997
15162
  this.updateDone.subscribe(() => {
13998
15163
  if (this.currentSpaceID) {
13999
15164
  this.initFeatures();
@@ -14384,6 +15549,19 @@ class MeasurementService {
14384
15549
  this.initMeasurements();
14385
15550
  }
14386
15551
  });
15552
+ // The Three.js viewer shows a whole space at once, with no zone to navigate into, so it
15553
+ // announces itself here instead of through `zoneChange`.
15554
+ this.zoneChangeService.spaceWithoutZones.subscribe((spaceID) => {
15555
+ this.currentSpaceID = spaceID;
15556
+ this.zoneIDFilter = null;
15557
+ if (!spaceID) {
15558
+ this.measurementsTags.next(null);
15559
+ this.measurementsUpdated.next({ space: [], zone: null });
15560
+ this.measurementFilter = null;
15561
+ return;
15562
+ }
15563
+ this.initMeasurements();
15564
+ });
14387
15565
  this.filterService.subscribeToDataFilterUpdate((dateRange) => {
14388
15566
  this.dateFilter = dateRange;
14389
15567
  this.initMeasurements();
@@ -14460,6 +15638,9 @@ class MeasurementService {
14460
15638
  type: PoiType.MEASURE,
14461
15639
  elementID: comment.id,
14462
15640
  matterportSweepID: sweepID,
15641
+ // Only the Three.js viewer sends this: it records which 3D model the measurement was taken
15642
+ // in, the same as createPoiForThreeJsPlacement does for the other tags.
15643
+ ...(dataMeasurements.metadata ? { metadata: dataMeasurements.metadata } : {}),
14463
15644
  };
14464
15645
  const poi = await this.poiService.createPOIForSpaceID(poiInput);
14465
15646
  const screen = await fetch(screenshot);
@@ -15163,11 +16344,11 @@ class NavigationBarComponent {
15163
16344
  this.baseVisibilityService.isChangePositionVisible.next(false);
15164
16345
  }
15165
16346
  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 });
15166
- 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" }] });
16347
+ 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" }] });
15167
16348
  }
15168
16349
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NavigationBarComponent, decorators: [{
15169
16350
  type: Component,
15170
- 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"] }]
16351
+ 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"] }]
15171
16352
  }], ctorParameters: () => [{ type: undefined, decorators: [{
15172
16353
  type: Inject,
15173
16354
  args: ['currentAwsKinesisAnalyticsService']
@@ -15436,11 +16617,11 @@ class MenuBarComponent {
15436
16617
  }
15437
16618
  }
15438
16619
  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 });
15439
- 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" }] });
16620
+ 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" }] });
15440
16621
  }
15441
16622
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MenuBarComponent, decorators: [{
15442
16623
  type: Component,
15443
- 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"] }]
16624
+ 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"] }]
15444
16625
  }], 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: [{
15445
16626
  type: Input
15446
16627
  }] } });
@@ -15595,27 +16776,27 @@ class BaseTagService {
15595
16776
  throw new Error('updatePoiMetadata is not implemented in the base class.');
15596
16777
  }*/
15597
16778
  async prepareEquipmentHtml(equip) {
15598
- throw new Error(`prepareEquipmentHtml is not implemented in the base class.
16779
+ throw new Error(`prepareEquipmentHtml is not implemented in the base class.
15599
16780
  Please override this method in your extended class.`);
15600
16781
  }
15601
16782
  async prepareTicketHtml(ticket) {
15602
- throw new Error(`prepareTicketHtml is not implemented in the base class.
16783
+ throw new Error(`prepareTicketHtml is not implemented in the base class.
15603
16784
  Please override this method in your extended class.`);
15604
16785
  }
15605
16786
  async prepareFeatureHtml(feature) {
15606
- throw new Error(`prepareFeatureHtml is not implemented in the base class.
16787
+ throw new Error(`prepareFeatureHtml is not implemented in the base class.
15607
16788
  Please override this method in your extended class.`);
15608
16789
  }
15609
16790
  async prepareMeasurementHtml(measure) {
15610
- throw new Error(`prepareMeasurementHtml is not implemented in the base class.
16791
+ throw new Error(`prepareMeasurementHtml is not implemented in the base class.
15611
16792
  Please override this method in your extended class.`);
15612
16793
  }
15613
16794
  async prepareDeskHtml(feature) {
15614
- throw new Error(`prepareDeskHtml is not implemented in the base class.
16795
+ throw new Error(`prepareDeskHtml is not implemented in the base class.
15615
16796
  Please override this method in your extended class.`);
15616
16797
  }
15617
16798
  async prepareIndicatorHtml(feature) {
15618
- throw new Error(`prepareDeskHtml is not implemented in the base class.
16799
+ throw new Error(`prepareDeskHtml is not implemented in the base class.
15619
16800
  Please override this method in your extended class.`);
15620
16801
  }
15621
16802
  async getSignedTagIconSource(tagIconSrc) {
@@ -15624,15 +16805,15 @@ class BaseTagService {
15624
16805
  getIconTagImageForFeature(feature, poi) {
15625
16806
  // const tagIcon = JSON.parse(poi.tagIcon);
15626
16807
  // return tagIcon.src;
15627
- throw new Error(`getIconTagImageForFeature is not implemented in the base class.
16808
+ throw new Error(`getIconTagImageForFeature is not implemented in the base class.
15628
16809
  Please override this method in your extended class.`);
15629
16810
  }
15630
16811
  getScriptForTag(object, tagType) {
15631
- throw new Error(`getScriptForTag is not implemented in the base class.
16812
+ throw new Error(`getScriptForTag is not implemented in the base class.
15632
16813
  Please override this method in your extended class.`);
15633
16814
  }
15634
16815
  getAnnexeForCommentTypeInFeature(feature, commentType) {
15635
- throw new Error(`getAnnexeForCommentTypeInFeature is not implemented in the base class.
16816
+ throw new Error(`getAnnexeForCommentTypeInFeature is not implemented in the base class.
15636
16817
  Please override this method in your extended class.`);
15637
16818
  }
15638
16819
  getBillboardMediaToEmbed(object) {
@@ -15641,27 +16822,27 @@ class BaseTagService {
15641
16822
  return { comment: null, tagDescription: '' };
15642
16823
  }
15643
16824
  onActionDetailClick(url) {
15644
- throw new Error(`onActionDetailClick is not implemented in the base class.
16825
+ throw new Error(`onActionDetailClick is not implemented in the base class.
15645
16826
  Please override this method in your extended class.`);
15646
16827
  }
15647
16828
  onActionAudioClick(audioCommentID) {
15648
- throw new Error(`onActionDetailClick is not implemented in the base class.
16829
+ throw new Error(`onActionDetailClick is not implemented in the base class.
15649
16830
  Please override this method in your extended class.`);
15650
16831
  }
15651
16832
  onActionVideoClick(url) {
15652
- throw new Error(`onActionVideoClick is not implemented in the base class.
16833
+ throw new Error(`onActionVideoClick is not implemented in the base class.
15653
16834
  Please override this method in your extended class.`);
15654
16835
  }
15655
16836
  onActionImageClick(imageCommentID) {
15656
- throw new Error(`onActionImageClick is not implemented in the base class.
16837
+ throw new Error(`onActionImageClick is not implemented in the base class.
15657
16838
  Please override this method in your extended class.`);
15658
16839
  }
15659
16840
  onActionDocClick(url) {
15660
- throw new Error(`onActionDocClick is not implemented in the base class.
16841
+ throw new Error(`onActionDocClick is not implemented in the base class.
15661
16842
  Please override this method in your extended class.`);
15662
16843
  }
15663
16844
  onActionYoutubeClick(url) {
15664
- throw new Error(`onActionYoutubeClick is not implemented in the base class.
16845
+ throw new Error(`onActionYoutubeClick is not implemented in the base class.
15665
16846
  Please override this method in your extended class.`);
15666
16847
  }
15667
16848
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BaseTagService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
@@ -19306,5 +20487,5 @@ function floatValidator() {
19306
20487
  * Generated bundle index. Do not edit.
19307
20488
  */
19308
20489
 
19309
- 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 };
20490
+ 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, SweepLocatorService, THREEJS_MODEL_UP_OPTIONS, 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, getThreeJsPoiOrigin, 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 };
19310
20491
  //# sourceMappingURL=smarterplan-ngx-smarterplan-core.mjs.map