@smarterplan/ngx-smarterplan-core 1.4.18 → 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 (102) hide show
  1. package/README.md +24 -24
  2. package/esm2022/lib/components/csv-export/csv-export.component.mjs +3 -3
  3. package/esm2022/lib/components/loader/loader.component.mjs +3 -3
  4. package/esm2022/lib/components/menu-bar/avatar/avatar.component.mjs +1 -1
  5. package/esm2022/lib/components/menu-bar/menu-bar.component.mjs +3 -3
  6. package/esm2022/lib/components/menu-bar/navigation-bar/navigation-bar.component.mjs +3 -3
  7. package/esm2022/lib/components/modal-switch-visit/modal-switch-visit.component.mjs +3 -3
  8. package/esm2022/lib/components/search-bar/search-bar.component.mjs +3 -3
  9. package/esm2022/lib/components/support-modal/support-modal.component.mjs +3 -3
  10. package/esm2022/lib/config.mjs +1 -1
  11. package/esm2022/lib/helpers.service.mjs +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 +18 -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 +21 -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 +1785 -583
  92. package/fesm2022/smarterplan-ngx-smarterplan-core.mjs.map +1 -1
  93. package/lib/helpers.service.d.ts +17 -0
  94. package/lib/services/matterport-navigation.service.d.ts +9 -0
  95. package/lib/services/models/base-object.service.d.ts +3 -4
  96. package/lib/services/models/measurement.service.d.ts +1 -0
  97. package/lib/services/sweep-locator.service.d.ts +54 -0
  98. package/lib/services/threejs.service.d.ts +304 -10
  99. package/lib/services/viewer.service.d.ts +87 -2
  100. package/package.json +1 -1
  101. package/public-api.d.ts +1 -0
  102. 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 = [];
@@ -2572,6 +2575,8 @@ class MatterportNavigationService {
2572
2575
  currentRooms = [];
2573
2576
  /** Sequence number of the floor currently visible in sdk.Floor.current (null = single-floor or unknown). */
2574
2577
  currentFloorSequence = null;
2578
+ /** True while a floorplan transition was explicitly requested through action_toolbox_floorplan. */
2579
+ floorplanRequested = false;
2575
2580
  // ── Bidirectional sid ↔ uuid lookup maps ──
2576
2581
  // sid = short runtime key used by Matterport SDK (collection keys, Sweep.moveTo, Camera.pose.sweep)
2577
2582
  // uuid = persistent identifier stored in the database (zone.sweepIDs, poi.matterportSweepID, startSweepID)
@@ -2638,12 +2643,27 @@ class MatterportNavigationService {
2638
2643
  console.log('viewer is in transition, cannot go floorplan');
2639
2644
  return;
2640
2645
  }
2646
+ this.floorplanRequested = true;
2641
2647
  try {
2642
2648
  await sdk.Mode.moveTo('mode.floorplan');
2643
2649
  }
2644
2650
  catch (e) {
2645
2651
  console.log('cannot go to floorplan', e);
2646
2652
  }
2653
+ finally {
2654
+ this.floorplanRequested = false;
2655
+ }
2656
+ }
2657
+ /**
2658
+ * When a dollhouse camera looks straight down, Matterport reports 'mode.floorplan'
2659
+ * (its "peekaboo" corrected mode) although the camera is still in dollhouse.
2660
+ * The native mode buttons are hidden, so such a switch that we did not request
2661
+ * is never a user action and must not be treated as a real mode change.
2662
+ */
2663
+ isUnrequestedFloorplanSwitch(previousMode, newMode) {
2664
+ return (previousMode === 'mode.dollhouse' &&
2665
+ newMode === 'mode.floorplan' &&
2666
+ !this.floorplanRequested);
2647
2667
  }
2648
2668
  action_toolbox_inside_view(sdk) {
2649
2669
  sdk.Mode.moveTo('mode.inside');
@@ -3010,11 +3030,11 @@ class MatterportPointerService {
3010
3030
  }
3011
3031
  updateCursorDisplay(poseMatterport) {
3012
3032
  if (this.textDisplayCursorPositionPanel && poseMatterport) {
3013
- this.textDisplayCursorPositionPanel.innerHTML = `position:
3014
- ${pointToString(poseMatterport.position)}\n
3015
- normal:
3016
- ${pointToString(poseMatterport.normal)}\n
3017
- floorId:
3033
+ this.textDisplayCursorPositionPanel.innerHTML = `position:
3034
+ ${pointToString(poseMatterport.position)}\n
3035
+ normal:
3036
+ ${pointToString(poseMatterport.normal)}\n
3037
+ floorId:
3018
3038
  ${poseMatterport.floorId}`;
3019
3039
  if (this.getCursorPositionButton) {
3020
3040
  this.getCursorPositionButton.style.display = 'none';
@@ -4645,6 +4665,10 @@ class MatterportService {
4645
4665
  }.bind(this));
4646
4666
  //Camera mode
4647
4667
  this.sdk.Mode.current.subscribe((mode) => {
4668
+ if (this.navigationService.isUnrequestedFloorplanSwitch(this.cameraMode, mode)) {
4669
+ // Top-down dollhouse camera reported as floorplan: still dollhouse, ignore it
4670
+ return;
4671
+ }
4648
4672
  if (this.cameraMode !== mode) {
4649
4673
  this.navigationService.setCameraMode(mode);
4650
4674
  // Refresh tag visibility on every mode change (INSIDE vs DOLLHOUSE vs FLOORPLAN)
@@ -5160,22 +5184,22 @@ class MatterportService {
5160
5184
  );
5161
5185
  }*/
5162
5186
  addCustomCss() {
5163
- const css = `
5164
- .footer-ui,
5165
- .showcase-footer,
5166
- .mp-nova-btn-group,
5167
- .footer-divider,
5168
- .logo-link,
5169
- .footer-logo,
5170
- .bottom-ui,
5171
- img.logo-image,
5172
- img[class^="logo-image"],
5173
- img[class*=" logo-image"],
5174
- [data-testid="showcase-footer"] {
5175
- opacity: 0 !important;
5176
- visibility: hidden !important;
5177
- pointer-events: none !important;
5178
- }
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
+ }
5179
5203
  `;
5180
5204
  // 1st method: Via Scene SDK
5181
5205
  if (this.sdk?.Scene?.addStyle) {
@@ -5773,20 +5797,29 @@ function frameFromPoints(a, b, c) {
5773
5797
  return new THREE.Matrix4().makeBasis(e1, e2, e1.clone().cross(e2));
5774
5798
  }
5775
5799
  /**
5776
- * 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.
5777
5802
  *
5778
- * Takes exactly the correspondences a person can produce by hand: three tags, each pointed at twice
5779
- * - once where it currently sits, once where it belongs. That is the minimum that pins down all
5780
- * three parts of the answer. One pair fixes only translation; two leave the rotation about the line
5781
- * between them free; three resolve it.
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:
5782
5806
  *
5783
- * Returns null when the points are collinear or degenerate, because a wrong alignment applied
5784
- * silently is worse than none.
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.
5810
+ *
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.
5785
5815
  */
5786
- function solveThreeJsAlignment(pairs) {
5787
- if (!pairs || pairs.length < 3) {
5816
+ function solveThreeJsAlignment(pairs, targetUp = { x: 0, y: 1, z: 0 }) {
5817
+ if (!pairs || pairs.length === 0) {
5788
5818
  return null;
5789
5819
  }
5820
+ if (pairs.length < 3) {
5821
+ return solveReducedAlignment(pairs, new THREE.Vector3(targetUp.x, targetUp.y, targetUp.z).normalize());
5822
+ }
5790
5823
  const sources = pairs.slice(0, 3).map((pair) => new THREE.Vector3(pair.source.x, pair.source.y, pair.source.z));
5791
5824
  const targets = pairs.slice(0, 3).map((pair) => new THREE.Vector3(pair.target.x, pair.target.y, pair.target.z));
5792
5825
  const sourceFrame = frameFromPoints(sources[0], sources[1], sources[2]);
@@ -5827,6 +5860,41 @@ function solveThreeJsAlignment(pairs) {
5827
5860
  scale,
5828
5861
  };
5829
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
+ }
5830
5898
  class ThreeJsService {
5831
5899
  ngZone;
5832
5900
  scene;
@@ -5864,6 +5932,61 @@ class ThreeJsService {
5864
5932
  pendingTagPlacement = null;
5865
5933
  /** Pointer position waiting for a preview pick, coalesced to one per frame. */
5866
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();
5867
5990
  /**
5868
5991
  * Layer carrying the model alone, so the depth pick can render it without the tags.
5869
5992
  *
@@ -5871,6 +5994,8 @@ class ThreeJsService {
5871
5994
  * their parent passed, so marking the model's own meshes is enough - no re-parenting.
5872
5995
  */
5873
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';
5874
5999
  /** Cache Storage bucket holding downloaded models, and how many to keep. */
5875
6000
  static MODEL_CACHE = 'smarterplan-3d-models';
5876
6001
  static MODEL_CACHE_SIZE = 3;
@@ -5887,6 +6012,8 @@ class ThreeJsService {
5887
6012
  aligning = false;
5888
6013
  alignmentPairs = [];
5889
6014
  alignmentSource = null;
6015
+ /** The tag `alignmentSource` was read from, kept so the same tag is not matched twice. */
6016
+ alignmentSourceTagId;
5890
6017
  /** Drives the alignment walkthrough's UI: how many pairs are done and what it wants next. */
5891
6018
  alignmentProgress$ = new Subject();
5892
6019
  /** In-flight camera move started by goToTag, advanced by the render loop. */
@@ -6002,6 +6129,8 @@ class ThreeJsService {
6002
6129
  // new model and show the previous visit's tags on it.
6003
6130
  this.discardCurrentLoad();
6004
6131
  this.clearTags();
6132
+ this.measuring = false;
6133
+ this.clearMeasurement();
6005
6134
  this.cameraTween = null;
6006
6135
  this.modelRoot = undefined;
6007
6136
  // 1. Setup Scene
@@ -6109,7 +6238,7 @@ class ThreeJsService {
6109
6238
  * and stays indexed, so far less data reaches the GPU. KTX2 textures are supported too: they
6110
6239
  * stay compressed in VRAM instead of being decoded to raw pixels like JPEG/PNG.
6111
6240
  */
6112
- loadGLTFModel(url, cameraPosition, rotation, cacheKey) {
6241
+ loadGLTFModel(url, cameraPosition, rotation, cacheKey, up) {
6113
6242
  const loader = new GLTFLoader();
6114
6243
  // KTX2 needs the renderer to report which compressed formats this GPU accepts, so this has to
6115
6244
  // happen after init(). Guarded so a model without KTX2 textures still loads if it's missing.
@@ -6135,7 +6264,7 @@ class ThreeJsService {
6135
6264
  this.forceDoubleSidedMaterials(gltf.scene);
6136
6265
  this.ensureVertexNormals(gltf.scene);
6137
6266
  this.tuneTextureSampling(gltf.scene);
6138
- this.placeLoadedObjectInScene(gltf.scene, cameraPosition, rotation);
6267
+ this.placeLoadedObjectInScene(gltf.scene, cameraPosition, rotation, up);
6139
6268
  this.modelLoading$.next(false);
6140
6269
  };
6141
6270
  const onError = (error) => {
@@ -6608,7 +6737,7 @@ class ThreeJsService {
6608
6737
  });
6609
6738
  }
6610
6739
  /** Centers, scales and adds a loaded object to the scene, then frames the camera on it. */
6611
- placeLoadedObjectInScene(object, cameraPosition, rotation) {
6740
+ placeLoadedObjectInScene(object, cameraPosition, rotation, up) {
6612
6741
  const box = new THREE.Box3().setFromObject(object);
6613
6742
  const center = box.getCenter(new THREE.Vector3());
6614
6743
  const size = box.getSize(new THREE.Vector3());
@@ -6623,12 +6752,25 @@ class ThreeJsService {
6623
6752
  if (rotation) {
6624
6753
  object.rotation.set(rotation.x, rotation.y, rotation.z);
6625
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));
6626
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();
6627
6768
  // Marks the model for the depth pick, which renders this layer alone - so tags and the ghost
6628
6769
  // itself cannot end up being what the cursor reports as the surface under it.
6629
6770
  object.traverse((child) => child.layers.enable(ThreeJsService.PICK_LAYER));
6630
6771
  centered.add(object);
6631
6772
  centered.add(this.tagGroup);
6773
+ centered.add(this.modelTagGroup);
6632
6774
  group.add(centered);
6633
6775
  group.scale.setScalar(scale);
6634
6776
  this.scene.add(group);
@@ -6644,6 +6786,98 @@ class ThreeJsService {
6644
6786
  this.controls.target.set(0, 0, 0);
6645
6787
  this.controls.update();
6646
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
+ }
6647
6881
  /**
6648
6882
  * Keeps the drawing buffer and camera matched to the canvas as it is resized.
6649
6883
  *
@@ -6669,6 +6903,7 @@ class ThreeJsService {
6669
6903
  this.camera.aspect = width / height;
6670
6904
  this.camera.updateProjectionMatrix();
6671
6905
  this.renderer.setSize(width, height);
6906
+ this.updateMeasureLineResolution();
6672
6907
  });
6673
6908
  this.canvasResizeObserver.observe(container);
6674
6909
  }
@@ -6680,9 +6915,16 @@ class ThreeJsService {
6680
6915
  * applied until solveAlignmentFromPairs() succeeds.
6681
6916
  */
6682
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
+ }
6683
6922
  this.aligning = true;
6684
6923
  this.alignmentPairs = [];
6685
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();
6686
6928
  if (this.renderer) {
6687
6929
  this.renderer.domElement.style.cursor = 'crosshair';
6688
6930
  }
@@ -6695,6 +6937,7 @@ class ThreeJsService {
6695
6937
  this.alignmentSource = null;
6696
6938
  this.pendingPreviewEvent = null;
6697
6939
  this.removePlacementPreview();
6940
+ this.refreshTagVisibility();
6698
6941
  if (this.renderer) {
6699
6942
  this.renderer.domElement.style.cursor = 'default';
6700
6943
  }
@@ -6714,12 +6957,22 @@ class ThreeJsService {
6714
6957
  * first half of a pair, which is a click on a tag, not on the model.
6715
6958
  */
6716
6959
  isPreviewingPlacement() {
6717
- return this.placingTag || (this.aligning && !!this.alignmentSource);
6960
+ return this.placingTag || this.measuring || (this.aligning && !!this.alignmentSource);
6718
6961
  }
6719
6962
  /** Whether a tag is currently registered for this element. */
6720
6963
  hasTag(elementId) {
6721
6964
  return this.tagSprites.has(elementId);
6722
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
+ }
6723
6976
  /**
6724
6977
  * Solves and applies the alignment collected so far, returning it so the caller can persist it.
6725
6978
  *
@@ -6727,35 +6980,105 @@ class ThreeJsService {
6727
6980
  * the walkthrough stays open for the person to pick a better third point.
6728
6981
  */
6729
6982
  solveAlignmentFromPairs() {
6730
- 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);
6731
6986
  if (!alignment) {
6732
6987
  return null;
6733
6988
  }
6734
- 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.
6735
6991
  this.aligning = false;
6992
+ this.applyAlignmentKeepingOwnTags(alignment);
6736
6993
  this.pendingPreviewEvent = null;
6737
6994
  this.removePlacementPreview();
6738
6995
  this.renderer.domElement.style.cursor = 'default';
6739
6996
  this.alignmentProgress$.next({ pairs: this.alignmentPairs.length, awaiting: null });
6740
6997
  return alignment;
6741
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
+ }
6742
7073
  /** Handles a click while the alignment walkthrough is running. */
6743
7074
  handleAlignmentClick(event) {
6744
7075
  if (!this.alignmentSource) {
6745
7076
  // First half of a pair: which tag are we talking about?
6746
- const hit = this.raycaster.intersectObjects(this.tagGroup.children, false)[0];
7077
+ const hit = this.raycaster.intersectObjects(this.calibrationCandidates(), false)[0];
6747
7078
  if (!hit) {
6748
7079
  return;
6749
7080
  }
6750
- // The sprite's own position is the stored coordinate, untouched by any alignment already
6751
- // applied - that transform lives on the group, not on the sprite.
6752
- const stored = hit.object.position;
6753
- this.alignmentSource = { x: stored.x, y: stored.y, z: stored.z };
6754
- this.alignmentProgress$.next({
6755
- pairs: this.alignmentPairs.length,
6756
- awaiting: 'point',
6757
- tagId: hit.object.name,
6758
- });
7081
+ this.setAlignmentSource(hit.object);
6759
7082
  return;
6760
7083
  }
6761
7084
  // Second half: where that tag actually belongs on the model.
@@ -6763,15 +7086,249 @@ class ThreeJsService {
6763
7086
  if (!target) {
6764
7087
  return;
6765
7088
  }
6766
- this.alignmentPairs.push({ source: this.alignmentSource, target });
7089
+ this.alignmentPairs.push({ tagId: this.alignmentSourceTagId, source: this.alignmentSource, target });
6767
7090
  this.alignmentSource = null;
6768
7091
  // The ghost belongs to the click that just happened; the next half is a click on a tag.
6769
7092
  this.pendingPreviewEvent = null;
6770
7093
  this.removePlacementPreview();
6771
7094
  this.alignmentProgress$.next({
6772
7095
  pairs: this.alignmentPairs.length,
6773
- 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?.();
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;
6774
7330
  });
7331
+ return canvas.toDataURL('image/jpeg', 0.85);
6775
7332
  }
6776
7333
  /** Arms tag placement: the next click on the model drops a tag instead of being ignored. */
6777
7334
  startTagPlacement(poiType = null) {
@@ -6802,6 +7359,7 @@ class ThreeJsService {
6802
7359
  const position = this.pickPointOnGpu(event);
6803
7360
  if (!position) {
6804
7361
  this.removePlacementPreview();
7362
+ this.updateMeasureRubberBand(null);
6805
7363
  this.renderer.domElement.style.cursor = 'not-allowed';
6806
7364
  return;
6807
7365
  }
@@ -6818,6 +7376,7 @@ class ThreeJsService {
6818
7376
  this.scaleTagForModel(this.placementPreview);
6819
7377
  }
6820
7378
  this.placementPreview.position.copy(position);
7379
+ this.updateMeasureRubberBand(position);
6821
7380
  }
6822
7381
  /**
6823
7382
  * The point under the cursor, read from the depth buffer instead of raycast, in tag space.
@@ -6919,9 +7478,250 @@ class ThreeJsService {
6919
7478
  * filter that happens to match nothing.
6920
7479
  */
6921
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;
6922
7487
  this.tagSprites.forEach((sprite) => {
6923
- 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
+ }
6924
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;
6925
7725
  }
6926
7726
  /**
6927
7727
  * Moves the camera to look at a tag, mirroring Matterport's goToTag.
@@ -7052,8 +7852,11 @@ class ThreeJsService {
7052
7852
  this.tagGroup.position.set(translation?.x ?? 0, translation?.y ?? 0, translation?.z ?? 0);
7053
7853
  this.tagGroup.rotation.set(rotation?.x ?? 0, rotation?.y ?? 0, rotation?.z ?? 0);
7054
7854
  this.tagGroup.scale.setScalar(scale ?? 1);
7055
- // 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.
7056
7858
  this.tagSprites.forEach((sprite) => this.scaleTagForModel(sprite));
7859
+ this.refreshTagVisibility();
7057
7860
  }
7058
7861
  /**
7059
7862
  * Adds a tag, or moves/restyles the one already registered for this element.
@@ -7068,6 +7871,10 @@ class ThreeJsService {
7068
7871
  existing.position.set(tag.position.x, tag.position.y, tag.position.z);
7069
7872
  existing.userData.poiType = tag.poiType ?? existing.userData.poiType;
7070
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);
7071
7878
  return;
7072
7879
  }
7073
7880
  const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
@@ -7080,10 +7887,13 @@ class ThreeJsService {
7080
7887
  sprite.name = tag.id;
7081
7888
  sprite.userData.poiType = tag.poiType ?? null;
7082
7889
  sprite.userData.label = tag.label ?? null;
7890
+ sprite.userData.originCaptureID = tag.originCaptureID ?? null;
7891
+ sprite.userData.frame = tag.frame ?? null;
7083
7892
  sprite.position.set(tag.position.x, tag.position.y, tag.position.z);
7084
7893
  sprite.renderOrder = 999;
7894
+ this.parentTag(sprite);
7085
7895
  this.scaleTagForModel(sprite);
7086
- this.tagGroup.add(sprite);
7896
+ this.applyTagVisibility(sprite);
7087
7897
  this.tagSprites.set(tag.id, sprite);
7088
7898
  if (tag.iconUrl) {
7089
7899
  this.applyTagIcon(sprite, tag.iconUrl);
@@ -7095,7 +7905,7 @@ class ThreeJsService {
7095
7905
  if (!sprite) {
7096
7906
  return;
7097
7907
  }
7098
- this.tagGroup.remove(sprite);
7908
+ sprite.parent?.remove(sprite);
7099
7909
  sprite.material.map?.dispose();
7100
7910
  sprite.material.dispose();
7101
7911
  this.tagSprites.delete(elementId);
@@ -7136,7 +7946,8 @@ class ThreeJsService {
7136
7946
  * size on screen whatever the model's units are.
7137
7947
  */
7138
7948
  scaleTagForModel(sprite) {
7139
- 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;
7140
7951
  const size = 0.4 / worldScale;
7141
7952
  sprite.scale.set(size, size, 1);
7142
7953
  }
@@ -7195,7 +8006,7 @@ class ThreeJsService {
7195
8006
  this.removePlacementPreview();
7196
8007
  // Tags first: they draw on top of the model, so they have to answer the pointer first too.
7197
8008
  const hoveredTag = this.raycaster
7198
- .intersectObjects(this.tagGroup.children, false)
8009
+ .intersectObjects([...this.tagSprites.values()], false)
7199
8010
  .find((hit) => hit.object.visible)?.object;
7200
8011
  if (hoveredTag) {
7201
8012
  this.renderer.domElement.style.cursor = 'pointer';
@@ -7234,6 +8045,15 @@ class ThreeJsService {
7234
8045
  this.handleAlignmentClick(event);
7235
8046
  return;
7236
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
+ }
7237
8057
  // Placement wins over everything: while armed, a click means "put it here", not "open that".
7238
8058
  if (this.placingTag) {
7239
8059
  const placement = this.pickPlacementOnModel(event);
@@ -7249,7 +8069,7 @@ class ThreeJsService {
7249
8069
  }
7250
8070
  this.raycaster.setFromCamera(this.mouse, this.camera);
7251
8071
  // Same precedence as hover: a tag drawn over the model claims the click.
7252
- const tagHits = this.raycaster.intersectObjects(this.tagGroup.children, false)
8072
+ const tagHits = this.raycaster.intersectObjects([...this.tagSprites.values()], false)
7253
8073
  .filter((hit) => hit.object.visible);
7254
8074
  if (tagHits.length > 0) {
7255
8075
  const tag = tagHits[0].object;
@@ -7284,6 +8104,10 @@ class ThreeJsService {
7284
8104
  this.cancelTagPlacement();
7285
8105
  return;
7286
8106
  }
8107
+ if (this.measuring) {
8108
+ this.cancelMeasuring();
8109
+ return;
8110
+ }
7287
8111
  if (this.aligning) {
7288
8112
  this.cancelTagAlignment();
7289
8113
  }
@@ -7311,6 +8135,8 @@ class ThreeJsService {
7311
8135
  }
7312
8136
  this.discardCurrentLoad();
7313
8137
  this.clearTags();
8138
+ this.measuring = false;
8139
+ this.clearMeasurement();
7314
8140
  if (this.renderer) {
7315
8141
  this.renderer.dispose();
7316
8142
  }
@@ -7328,7 +8154,471 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
7328
8154
  }]
7329
8155
  }], ctorParameters: () => [{ type: i0.NgZone }] });
7330
8156
 
7331
- /* eslint-disable no-await-in-loop */
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 */
7332
8622
  /* eslint-disable camelcase */
7333
8623
  /* eslint-disable no-underscore-dangle */
7334
8624
  /* eslint-disable class-methods-use-this */
@@ -7338,6 +8628,7 @@ class ViewerService {
7338
8628
  poiService;
7339
8629
  matterportService;
7340
8630
  threeJsService;
8631
+ sweepLocator;
7341
8632
  viewerMode = 'Matterport';
7342
8633
  currentModel3D;
7343
8634
  tourUrl;
@@ -7353,6 +8644,20 @@ class ViewerService {
7353
8644
  * lists that feed tags are exactly that.
7354
8645
  */
7355
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;
7356
8661
  sweepToMove;
7357
8662
  rotationToMove;
7358
8663
  goToTriggered = new Subject();
@@ -7381,12 +8686,13 @@ class ViewerService {
7381
8686
  },
7382
8687
  };
7383
8688
  isObjectLoaded = false;
7384
- constructor(sdkKey, config, poiService, matterportService, threeJsService) {
8689
+ constructor(sdkKey, config, poiService, matterportService, threeJsService, sweepLocator) {
7385
8690
  this.sdkKey = sdkKey;
7386
8691
  this.config = config;
7387
8692
  this.poiService = poiService;
7388
8693
  this.matterportService = matterportService;
7389
8694
  this.threeJsService = threeJsService;
8695
+ this.sweepLocator = sweepLocator;
7390
8696
  this.setMode('Matterport');
7391
8697
  }
7392
8698
  /*-----------------------------------------------------------------------------------------------------------------
@@ -7592,14 +8898,77 @@ class ViewerService {
7592
8898
  if (![position?.x, position?.y, position?.z].every((value) => typeof value === 'number')) {
7593
8899
  return;
7594
8900
  }
8901
+ const origin = getThreeJsPoiOrigin(poi);
7595
8902
  this.threeJsService.upsertTag({
7596
8903
  id: object.id,
7597
8904
  position,
7598
8905
  poiType: poi.type,
7599
8906
  label: object.name ?? object.title ?? null,
7600
8907
  iconUrl: await this.resolveTagIconUrl(poi),
8908
+ originCaptureID: origin?.captureID ?? null,
8909
+ frame: origin?.frame ?? null,
7601
8910
  });
7602
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
+ }
7603
8972
  /**
7604
8973
  * The image to draw a tag with, mirroring what Matterport shows for the same POI.
7605
8974
  *
@@ -7692,6 +9061,7 @@ class ViewerService {
7692
9061
  if (!alignment) {
7693
9062
  return null;
7694
9063
  }
9064
+ this.threeJsCalibrated = true;
7695
9065
  // Merged into whatever the capture already stores, so an existing camera placement survives.
7696
9066
  let stored = {};
7697
9067
  try {
@@ -7746,7 +9116,11 @@ class ViewerService {
7746
9116
  if (!this.checkMode('threejs')) {
7747
9117
  return;
7748
9118
  }
7749
- 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);
7750
9124
  }
7751
9125
  /**
7752
9126
  * Enable tag placement menu to add a new element depending on its type (TICKET, DATA, EQUIPMENT etc)
@@ -7815,22 +9189,116 @@ class ViewerService {
7815
9189
  if (this.checkMode('Matterport')) {
7816
9190
  await this.matterportService.action_toolbox_floorplan();
7817
9191
  }
7818
- }
7819
- action_toolbox_mesure() {
7820
- if (this.matterportService.getInteractionMode() !== ViewerInteractions.DEFAULT) {
7821
- 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) : {};
7822
9284
  }
7823
- this.action_toolbox_inside_view();
7824
- // Matterport
7825
- if (this.checkMode('Matterport')) {
7826
- this.matterportService.action_toolbox_mesure();
9285
+ catch {
9286
+ stored = {};
7827
9287
  }
9288
+ stored.up = up;
9289
+ return { up, metadata: JSON.stringify(stored) };
7828
9290
  }
7829
- action_toolbox_cancel_mesure() {
7830
- // Matterport
7831
- if (this.checkMode('Matterport')) {
7832
- this.matterportService.action_toolbox_cancel_mesure();
7833
- }
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;
7834
9302
  }
7835
9303
  async action_update_poi(poi, isValid, element) {
7836
9304
  if (this.checkMode('Matterport')) {
@@ -7920,11 +9388,15 @@ class ViewerService {
7920
9388
  if (!placement || !element?.id) {
7921
9389
  return null;
7922
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);
7923
9394
  const poi = await this.poiService.createPOIForSpaceID({
7924
9395
  coordinate: JSON.stringify(placement.position),
7925
9396
  type: poiType,
7926
9397
  elementID: element.id,
7927
- metadata: JSON.stringify({ normal: placement.normal }),
9398
+ metadata: this.buildThreeJsPoiMetadata({ normal: placement.normal }),
9399
+ ...(matterportSweepID ? { matterportSweepID } : {}),
7928
9400
  });
7929
9401
  // Draw it straight away rather than waiting for the next refresh, so the user sees where the
7930
9402
  // thing they just created ended up. Through the same path the refresh uses, so it arrives with
@@ -8054,7 +9526,7 @@ class ViewerService {
8054
9526
  const placement = parseThreeJsPlacement(placementMetadata);
8055
9527
  // The key, not the signed URL, is what identifies the model across loads - the signature makes
8056
9528
  // every URL unique, which is exactly why the browser's own cache never helps here.
8057
- this.threeJsService.loadGLTFModel(url, placement?.cameraPosition, placement?.rotation, key);
9529
+ this.threeJsService.loadGLTFModel(url, placement?.cameraPosition, placement?.rotation, key, placement?.up);
8058
9530
  }
8059
9531
  /**
8060
9532
  * Loads a 3D model into the Three.js scene (OBJ).
@@ -8149,7 +9621,7 @@ class ViewerService {
8149
9621
  isObject3DLoadedInPosition() {
8150
9622
  return this.isObjectLoaded;
8151
9623
  }
8152
- 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 });
8153
9625
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, providedIn: 'root' });
8154
9626
  }
8155
9627
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, decorators: [{
@@ -8163,7 +9635,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
8163
9635
  }] }, { type: Config, decorators: [{
8164
9636
  type: Inject,
8165
9637
  args: ['config']
8166
- }] }, { type: PoiService }, { type: MatterportService }, { type: ThreeJsService }] });
9638
+ }] }, { type: PoiService }, { type: MatterportService }, { type: ThreeJsService }, { type: SweepLocatorService }] });
8167
9639
 
8168
9640
  class ModalSwitchVisitComponent {
8169
9641
  activeModal;
@@ -8191,11 +9663,11 @@ class ModalSwitchVisitComponent {
8191
9663
  });
8192
9664
  }
8193
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 });
8194
- 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" }] });
8195
9667
  }
8196
9668
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ModalSwitchVisitComponent, decorators: [{
8197
9669
  type: Component,
8198
- 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"] }]
8199
9671
  }], ctorParameters: () => [{ type: i1$2.NgbActiveModal }, { type: i1$1.Router }, { type: ViewerService }], propDecorators: { content: [{
8200
9672
  type: Input
8201
9673
  }], model3D: [{
@@ -8206,6 +9678,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
8206
9678
  type: Input
8207
9679
  }] } });
8208
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"];
8209
9702
  /**
8210
9703
  * Parses the placement stored in a Three.js capture's metadata, or null when there is none.
8211
9704
  *
@@ -8244,7 +9737,12 @@ function parseThreeJsPlacement(metadata) {
8244
9737
  placement.alignment = alignment;
8245
9738
  }
8246
9739
  }
8247
- 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;
8248
9746
  }
8249
9747
  catch {
8250
9748
  console.warn("Ignoring unparseable Three.js placement metadata:", metadata);
@@ -8669,285 +10167,57 @@ function shuffleArray(array) {
8669
10167
  const newArray = [...array];
8670
10168
  for (let index = array.length - 1; index > 0; index--) {
8671
10169
  const indexNew = Math.floor(Math.random() * (index + 1));
8672
- // eslint-disable-next-line no-param-reassign
8673
- [newArray[index], newArray[indexNew]] = [
8674
- newArray[indexNew],
8675
- newArray[index],
8676
- ];
8677
- }
8678
- return newArray;
8679
- }
8680
- function numberToDateString(timestamp) {
8681
- const jsDate = new Date(timestamp);
8682
- return jsDate.toLocaleString("fr-FR");
8683
- }
8684
- function durationToString(duration, translate) {
8685
- const days = Math.floor(duration / 3600 / 24);
8686
- const hours = Math.floor((duration - days * 24 * 3600) / 3600);
8687
- const minutes = Math.floor((duration % 3600) / 60);
8688
- const seconds = Math.floor(duration % 60);
8689
- const daysString = translate.instant("day(s)");
8690
- const hoursString = translate.instant("hour(s)");
8691
- const oneHourString = translate.instant("hour");
8692
- const minutesString = translate.instant("minute(s)");
8693
- const secondsString = translate.instant("second(s)");
8694
- if (duration > 3600 * 24 * 7) {
8695
- return `${days} ${daysString}`;
8696
- }
8697
- if (duration > 3600 * 24) {
8698
- return `${days} ${daysString} ${hours} ${hoursString}`;
8699
- }
8700
- if (duration === 3600) {
8701
- return `${hours} ${oneHourString}`;
8702
- }
8703
- if (duration > 3600) {
8704
- if (minutes === 0) {
8705
- return `${hours} ${hoursString}`;
8706
- }
8707
- return `${hours} ${hoursString} ${minutes} ${minutesString}`;
8708
- }
8709
- if (duration >= 60) {
8710
- if (seconds === 0) {
8711
- return `${minutes} ${minutesString}`;
8712
- }
8713
- return `${minutes} ${minutesString} ${seconds} ${secondsString}`;
8714
- }
8715
- return `${seconds} ${secondsString}`;
8716
- }
8717
- function pointToString(point) {
8718
- const x = point.x.toFixed(3);
8719
- const y = point.y.toFixed(3);
8720
- const z = point.z.toFixed(3);
8721
- return `{ x: ${x}, y: ${y}, z: ${z} }`;
8722
- }
8723
-
8724
- /* eslint-disable class-methods-use-this */
8725
- class ZoneService {
8726
- userService;
8727
- API; //AWS APIService
8728
- constructor(apiInjected, userService) {
8729
- this.userService = userService;
8730
- this.API = apiInjected;
8731
- }
8732
- async getZonesBySpace(spaceID) {
8733
- const list = [];
8734
- if (this.API.__proto__.hasOwnProperty('ZonesBySpace')) {
8735
- // Paginated: without a nextToken loop, only the first page (100 zones) was returned and
8736
- // the sweeps of the other zones silently disappeared.
8737
- let nextToken = null;
8738
- do {
8739
- const zones = await this.API.__proto__.ZonesBySpace(spaceID, null, null, 1000, nextToken);
8740
- for (const zone of zones?.items || []) {
8741
- const zoneObject = { ...zone };
8742
- zoneObject.virtual = this.zoneIsVirtual(zone);
8743
- list.push(zoneObject);
8744
- }
8745
- nextToken = zones?.nextToken ?? null;
8746
- } while (nextToken);
8747
- return list;
8748
- }
8749
- else {
8750
- throw new Error('ZonesBySpace not implemented');
8751
- }
8752
- }
8753
- async getZonesBySpaceForUser(spaceID) {
8754
- const missionForLocation = this.userService.currentMission(spaceID);
8755
- const list = [];
8756
- if (missionForLocation && missionForLocation.zoneID) {
8757
- const allowedZone = await this.getZone(missionForLocation.zoneID);
8758
- allowedZone.virtual = this.zoneIsVirtual(allowedZone);
8759
- if (!allowedZone.parent) {
8760
- // the whole building is authorized
8761
- return this.getZonesBySpace(spaceID);
8762
- }
8763
- list.push(allowedZone);
8764
- // allow all children zones of an allowed parent
8765
- const children = (await this.API.__proto__.ZonesByZone(allowedZone.id))
8766
- .items;
8767
- if (children.length > 0) {
8768
- for (const child of children) {
8769
- const childZone = { ...child };
8770
- childZone.virtual = this.zoneIsVirtual(child);
8771
- list.push(childZone);
8772
- }
8773
- }
8774
- // allow the parent Floor of the allowed zone
8775
- if (allowedZone.parent.layer &&
8776
- allowedZone.parent.layer.name === 'FLOOR') {
8777
- list.push(allowedZone.parent);
8778
- }
8779
- }
8780
- else {
8781
- return this.getZonesBySpace(spaceID);
8782
- }
8783
- return list;
8784
- }
8785
- async filterZonesIDsForUser(zones) {
8786
- const { spaceID } = zones[0];
8787
- const zoneIDs = new Set(zones.map((zone) => zone.id));
8788
- const allowedZones = await this.getZonesBySpaceForUser(spaceID);
8789
- const filteredZones = allowedZones.filter((zone) => zoneIDs.has(zone.id));
8790
- return filteredZones;
8791
- }
8792
- async deleteZone(zoneID) {
8793
- return this.API.__proto__.DeleteZone({ id: zoneID });
8794
- }
8795
- async getZonesForSweepForSpace(sweepID, spaceID) {
8796
- const zonesSpace = (await this.API.__proto__.ZonesBySpace(spaceID)).items;
8797
- return zonesSpace.filter((zone) => zone.sweepIDs && zone.sweepIDs.includes(sweepID));
8798
- }
8799
- // // eslint-disable-next-line class-methods-use-this
8800
- sortZonesFromChildToParent(zonesToSort) {
8801
- // get default
8802
- const defaultZone = zonesToSort.find((z) => !z.parentID);
8803
- if (defaultZone) {
8804
- const indexOf = zonesToSort.indexOf(defaultZone);
8805
- zonesToSort.splice(indexOf, 1);
8806
- }
8807
- const zones = zonesToSort.sort((a, b) => {
8808
- if (a.parentID === b.id) {
8809
- return -1;
8810
- }
8811
- return 0;
8812
- });
8813
- // so far by number of navigation points in them
8814
- // const zones = zonesToSort.sort(
8815
- // (a, b) => a.navigations.items.length - b.navigations.items.length,
8816
- // );
8817
- if (defaultZone) {
8818
- zones.push(defaultZone);
8819
- }
8820
- return zones;
8821
- }
8822
- async getZonesForObject(object) {
8823
- if (object.pois.items && object.pois.items.length > 0) {
8824
- const [poi] = object.pois.items;
8825
- if (poi.matterportSweepID) {
8826
- const zones = await this.getZonesForSweepForSpace(poi.matterportSweepID, object.spaceID);
8827
- return zones;
8828
- }
8829
- }
8830
- return null;
8831
- }
8832
- async createVirtualZone(zoneInput) {
8833
- zoneInput.sweepIDs = [];
8834
- const zone = await this.API.__proto__.CreateZone(zoneInput);
8835
- console.log('virtual zone created', zone);
8836
- return zone;
8837
- }
8838
- zoneIsVirtual(zone) {
8839
- if (!zone.sweepIDs) {
8840
- return true;
8841
- }
8842
- if ((zone.tags && zone.tags.includes('virtual')) ||
8843
- zone.sweepIDs.length === 0) {
8844
- return true;
8845
- }
8846
- return false;
8847
- }
8848
- async getModel3DForZone(zoneID) {
8849
- const zone = await this.API.__proto__.GetZone(zoneID);
8850
- if (zone.nodeIDs) {
8851
- const node = await this.API.__proto__.GetNode(zone.nodeIDs[0]);
8852
- return node.model3D;
8853
- }
8854
- return null;
8855
- }
8856
- async getZone(zoneID) {
8857
- return this.API.__proto__.GetZone(zoneID);
8858
- }
8859
- async getDefaultZoneForSpace(spaceID) {
8860
- return this.getZonesBySpace(spaceID).then(async (zones) => {
8861
- const zonesBuilding = zones.filter((zone) => zone.layer && zone.layer.name === 'BUILDING');
8862
- if (zonesBuilding.length === 1) {
8863
- return zonesBuilding[0];
8864
- }
8865
- const parentZone = zones.find((zone) => !zone.parentID);
8866
- return parentZone;
8867
- });
8868
- }
8869
- async create(input) {
8870
- return this.API.__proto__.CreateZone(input);
8871
- }
8872
- // async getSortedZonesForSweepForUser(
8873
- // sweep: string,
8874
- // spaceID: string,
8875
- // ): Promise<Zone[]> {
8876
- // if (!spaceID) {
8877
- // return [];
8878
- // }
8879
- // const zones = await this.getZonesForSweepForSpace(sweep, spaceID);
8880
- // if (zones.length === 0) {
8881
- // return [];
8882
- // }
8883
- // const filteredZones = await this.filterZonesIDsForUser(zones);
8884
- // if (filteredZones.length > 0) {
8885
- // let zonesSorted = this.sortZonesFromChildToParent(filteredZones);
8886
- // zonesSorted = zonesSorted.filter((zone) => zone !== undefined);
8887
- // return zonesSorted;
8888
- // }
8889
- // return [];
8890
- // }
8891
- async updateZone(input) {
8892
- 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
+ ];
8893
10175
  }
8894
- async getZonesForNodeForSpace(nodeID, spaceID) {
8895
- return this.API.__proto__.ZonesBySpace(spaceID).then(async (zones) => {
8896
- const filteredZones = zones.items.filter((zone) => zone.nodeIDs && zone.nodeIDs.includes(nodeID));
8897
- return filteredZones;
8898
- });
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}`;
8899
10194
  }
8900
- /**
8901
- * Gets the Zone (layer FLOOR) for a matterport sweep ID in a given Space
8902
- * @param sweepID
8903
- * @param spaceID
8904
- * @returns Zone
8905
- */
8906
- async getFloorZoneForSweepInSpace(sweepID, spaceID) {
8907
- const zones = await this.getZonesForSweepForSpace(sweepID, spaceID);
8908
- const floors = zones.filter((zone) => zone.layer && zone.layer.name.includes('FLOOR'));
8909
- return floors.length > 0 ? floors[0] : null;
10195
+ if (duration > 3600 * 24) {
10196
+ return `${days} ${daysString} ${hours} ${hoursString}`;
8910
10197
  }
8911
- /**
8912
- * Get zones with audio track for entire space
8913
- * @param spaceID
8914
- * @returns Promise<Zone[]>
8915
- */
8916
- async getAudioZonesForSpace(spaceID) {
8917
- const allZones = await this.getZonesBySpace(spaceID);
8918
- return allZones.filter((zone) => zone.audioID);
10198
+ if (duration === 3600) {
10199
+ return `${hours} ${oneHourString}`;
8919
10200
  }
8920
- /**
8921
- * Get audio zones for user (within allowed zones)
8922
- * @param spaceID
8923
- * @returns Promise<Zone[]>
8924
- */
8925
- async getAudioZonesForUserForSpace(spaceID) {
8926
- const userZones = await this.getZonesBySpaceForUser(spaceID);
8927
- 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}`;
8928
10206
  }
8929
- /**
8930
- * Get Zones by Parent Zone ID
8931
- * @param parentZoneID string
8932
- * @returns Promise<Zone[]>
8933
- */
8934
- async getZonesByParentZone(parentZoneID) {
8935
- return this.API.__proto__
8936
- .ZonesByZone(parentZoneID)
8937
- .then((res) => res.items);
10207
+ if (duration >= 60) {
10208
+ if (seconds === 0) {
10209
+ return `${minutes} ${minutesString}`;
10210
+ }
10211
+ return `${minutes} ${minutesString} ${seconds} ${secondsString}`;
8938
10212
  }
8939
- 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 });
8940
- 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} }`;
8941
10220
  }
8942
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ZoneService, decorators: [{
8943
- type: Injectable,
8944
- args: [{
8945
- providedIn: 'root',
8946
- }]
8947
- }], ctorParameters: () => [{ type: undefined, decorators: [{
8948
- type: Inject,
8949
- args: ['currentAPIService']
8950
- }] }, { type: BaseUserService }] });
8951
10221
 
8952
10222
  /* eslint-disable no-case-declarations */
8953
10223
  /* eslint-disable no-await-in-loop */
@@ -9165,7 +10435,12 @@ class FilterService {
9165
10435
  // visibility filter already reads "no sweep" as "not floor-scoped, show it";
9166
10436
  // this is the same rule applied to zones.
9167
10437
  if (poi && poi.coordinate && !poi.matterportSweepID) {
9168
- 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
+ }
9169
10444
  return;
9170
10445
  }
9171
10446
  if (poi && zone) {
@@ -9560,133 +10835,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
9560
10835
  }]
9561
10836
  }], ctorParameters: () => [{ type: BaseUserService }, { type: ZoneService }, { type: PoiService }] });
9562
10837
 
9563
- class NavigationService {
9564
- userService;
9565
- zoneService;
9566
- API;
9567
- constructor(apiInjected, userService, zoneService) {
9568
- this.userService = userService;
9569
- this.zoneService = zoneService;
9570
- this.API = apiInjected;
9571
- }
9572
- async createNavigation(navInput) {
9573
- return this.API.__proto__.CreateNavigation(navInput);
9574
- }
9575
- async deleteNavigation(id) {
9576
- return this.API.__proto__.DeleteNavigation({ id });
9577
- }
9578
- async getNavigationsForNode(nodeID) {
9579
- const limit = 200;
9580
- let navigationsTokenDict = { navigations: [], nextToken: null };
9581
- do {
9582
- navigationsTokenDict =
9583
- await this.mergeAPIResponseWithNextTokenWithLimit(nodeID, limit, navigationsTokenDict);
9584
- } while (navigationsTokenDict.nextToken);
9585
- return navigationsTokenDict.navigations;
9586
- }
9587
- async mergeAPIResponseWithNextTokenWithLimit(nodeID, limit, navigationsTokenDict) {
9588
- return this.API.__proto__.NavigationsByNode(nodeID, null, null, limit, navigationsTokenDict.nextToken).then((response) => {
9589
- const navigations = response.items;
9590
- const navObject = {
9591
- navigations: [...navigationsTokenDict.navigations, ...navigations],
9592
- nextToken: response.nextToken,
9593
- };
9594
- return navObject;
9595
- });
9596
- }
9597
- async getNavigation(id) {
9598
- return this.API.__proto__.GetNavigation(id);
9599
- }
9600
- async getSweepsIDsForSpace(spaceID) {
9601
- const zones = await this.zoneService.getZonesBySpace(spaceID);
9602
- const zonesWithSweeps = zones.filter((zone) => zone.sweepIDs && zone.sweepIDs.length > 0);
9603
- const allSweeps = zonesWithSweeps.flatMap((zone) => zone.sweepIDs);
9604
- return [...new Set(allSweeps)];
9605
- }
9606
- async getAllowedSweepsIDsForUserInSpace(spaceID) {
9607
- const missionsForSpace = this.userService.currentMissions.filter((mis) => mis.spaceID === spaceID);
9608
- console.log(missionsForSpace);
9609
- let navIDs = [];
9610
- if (this.userService.userHasAccessToAllZonesInSpace(spaceID)) {
9611
- console.log(1);
9612
- return this.getSweepsIDsForSpace(spaceID);
9613
- }
9614
- await Promise.all(missionsForSpace.map(async (mission) => {
9615
- navIDs = [...navIDs, ...mission.zone.sweepIDs];
9616
- }));
9617
- console.log('final navids = ', navIDs);
9618
- return navIDs;
9619
- }
9620
- async getForbiddenNavigationIDsForUserInSpace(spaceID) {
9621
- const allowed = await this.getAllowedSweepsIDsForUserInSpace(spaceID);
9622
- const allNavs = await this.getSweepsIDsForSpace(spaceID);
9623
- const difference = allNavs.filter((x) => !allowed.includes(x));
9624
- return difference.length > 0 ? difference : null;
9625
- }
9626
- async getNavigationsForMatterportIDForSpace(matterportID, spaceID) {
9627
- return this.API.__proto__.NavigationsByMatterportIDBySpace(matterportID, {
9628
- eq: spaceID,
9629
- }).then((navs) => navs.items);
9630
- }
9631
- async getNavigationsForZone(zone) {
9632
- const responseNavs = [];
9633
- await Promise.all(zone.sweepIDs.map(async (sweepID) => {
9634
- const navs = await this.getNavigationsForMatterportIDForSpace(sweepID, zone.spaceID);
9635
- if (navs.length > 0) {
9636
- responseNavs.push(navs[0]);
9637
- }
9638
- }));
9639
- return responseNavs;
9640
- }
9641
- async updateNavigation(navInput) {
9642
- return this.API.__proto__.UpdateNavigation(navInput);
9643
- }
9644
- 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 });
9645
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NavigationService, providedIn: "root" });
9646
- }
9647
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NavigationService, decorators: [{
9648
- type: Injectable,
9649
- args: [{
9650
- providedIn: "root",
9651
- }]
9652
- }], ctorParameters: () => [{ type: undefined, decorators: [{
9653
- type: Inject,
9654
- args: ['currentAPIService']
9655
- }] }, { type: BaseUserService }, { type: ZoneService }] });
9656
-
9657
- class CaptureService {
9658
- API; //AWS
9659
- constructor(apiInjected) {
9660
- this.API = apiInjected;
9661
- }
9662
- async getCapturesBySpace(spaceID) {
9663
- return this.API.CapturesBySpace(spaceID).then((cap) => cap.items);
9664
- }
9665
- async getCapture(captureID) {
9666
- return this.API.__proto__.GetCapture(captureID);
9667
- }
9668
- async deleteCapture(captureID) {
9669
- return this.API.__proto__.DeleteCapture({ id: captureID });
9670
- }
9671
- async updateCapture(capture) {
9672
- return this.API.__proto__.UpdateCapture(capture);
9673
- }
9674
- async createCapture(capture) {
9675
- return this.API.__proto__.CreateCapture(capture);
9676
- }
9677
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CaptureService, deps: [{ token: 'currentAPIService' }], target: i0.ɵɵFactoryTarget.Injectable });
9678
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CaptureService, providedIn: "root" });
9679
- }
9680
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CaptureService, decorators: [{
9681
- type: Injectable,
9682
- args: [{
9683
- providedIn: "root",
9684
- }]
9685
- }], ctorParameters: () => [{ type: undefined, decorators: [{
9686
- type: Inject,
9687
- args: ['currentAPIService']
9688
- }] }] });
9689
-
9690
10838
  class NodeService {
9691
10839
  API;
9692
10840
  constructor(apiInjected) {
@@ -11022,7 +12170,7 @@ class BaseObjectService {
11022
12170
  // is no sweep to resolve a visit from, which is why this used to report the model
11023
12171
  // as unreachable. The space's 3D visit is the one that can show it.
11024
12172
  if (poi && poi.coordinate && !poi.matterportSweepID) {
11025
- const visit = await this.getThreeJsVisitForSpace(object.spaceID);
12173
+ const visit = await this.getThreeJsVisitForSpace(object.spaceID, getThreeJsPoiOrigin(poi)?.captureID);
11026
12174
  return visit
11027
12175
  ? { model3D: visit.model3d, sweep: null, rotation: null }
11028
12176
  : null;
@@ -11058,15 +12206,15 @@ class BaseObjectService {
11058
12206
  });
11059
12207
  }
11060
12208
  /**
11061
- * 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.
11062
12210
  *
11063
- * Takes the first one the database returns. That is not the space's chosen default - the
11064
- * default ordering lives in SpaceService, which cannot be injected here. The POI does not record
11065
- * 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.
11066
12213
  */
11067
- async getThreeJsVisitForSpace(spaceID) {
11068
- const visits = await this.visitService.getVisitsForSpaceForUser(spaceID, false);
11069
- 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;
11070
12218
  }
11071
12219
  async updateIconForAllObjects(objects, tagIcon) {
11072
12220
  const IDs = [];
@@ -12462,7 +13610,7 @@ class EquipmentService extends BaseObjectService {
12462
13610
  user: this.userService.cu,
12463
13611
  locationId: this.currentSpaceID,
12464
13612
  timestamp: new Date(),
12465
- zoneId: this.currentZone.id,
13613
+ zoneId: this.currentZone?.id,
12466
13614
  floorId: this.selectedFloor?.id,
12467
13615
  },
12468
13616
  streamName: 'smarterplanKinesis-analytics',
@@ -12926,6 +14074,11 @@ class EquipmentService extends BaseObjectService {
12926
14074
  }
12927
14075
  updateDueToCreate(createdEquip) {
12928
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
+ }
12929
14082
  this.currentEquipments.zonesMap
12930
14083
  .get(this.zoneIDFilter)
12931
14084
  .unshift(createdEquip);
@@ -12999,6 +14152,26 @@ class EquipmentService extends BaseObjectService {
12999
14152
  }
13000
14153
  }
13001
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
+ });
13002
14175
  this.deleteObservable
13003
14176
  .pipe(takeUntil(this.destroy$))
13004
14177
  .subscribe((equipment) => {
@@ -13213,11 +14386,11 @@ class SearchBarComponent {
13213
14386
  this.searchEvent.emit(results);
13214
14387
  }
13215
14388
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SearchBarComponent, deps: [{ token: SearchService }], target: i0.ɵɵFactoryTarget.Component });
13216
- 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" }] });
13217
14390
  }
13218
14391
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SearchBarComponent, decorators: [{
13219
14392
  type: Component,
13220
- 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"] }]
13221
14394
  }], ctorParameters: () => [{ type: SearchService }], propDecorators: { searchable: [{
13222
14395
  type: Input
13223
14396
  }], objectType: [{
@@ -13665,11 +14838,11 @@ class LoaderComponent {
13665
14838
  document.documentElement.style.setProperty('--color', this.color);
13666
14839
  }
13667
14840
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LoaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13668
- 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"] }] });
13669
14842
  }
13670
14843
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LoaderComponent, decorators: [{
13671
14844
  type: Component,
13672
- 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"] }]
13673
14846
  }], ctorParameters: () => [], propDecorators: { useLogo: [{
13674
14847
  type: Input
13675
14848
  }], color: [{
@@ -13727,11 +14900,11 @@ class SupportModalComponent {
13727
14900
  }
13728
14901
  }
13729
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 });
13730
- 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" }] });
13731
14904
  }
13732
14905
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SupportModalComponent, decorators: [{
13733
14906
  type: Component,
13734
- 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"] }]
13735
14908
  }], ctorParameters: () => [{ type: SupportService }, { type: i1$2.NgbActiveModal }, { type: i3.UntypedFormBuilder }, { type: BaseUserService }, { type: ValidatorsService }] });
13736
14909
 
13737
14910
  class LocaleService {
@@ -13973,6 +15146,19 @@ class FeatureService extends BaseObjectService {
13973
15146
  this.initFeatures();
13974
15147
  }
13975
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
+ });
13976
15162
  this.updateDone.subscribe(() => {
13977
15163
  if (this.currentSpaceID) {
13978
15164
  this.initFeatures();
@@ -14363,6 +15549,19 @@ class MeasurementService {
14363
15549
  this.initMeasurements();
14364
15550
  }
14365
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
+ });
14366
15565
  this.filterService.subscribeToDataFilterUpdate((dateRange) => {
14367
15566
  this.dateFilter = dateRange;
14368
15567
  this.initMeasurements();
@@ -14439,6 +15638,9 @@ class MeasurementService {
14439
15638
  type: PoiType.MEASURE,
14440
15639
  elementID: comment.id,
14441
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 } : {}),
14442
15644
  };
14443
15645
  const poi = await this.poiService.createPOIForSpaceID(poiInput);
14444
15646
  const screen = await fetch(screenshot);
@@ -15142,11 +16344,11 @@ class NavigationBarComponent {
15142
16344
  this.baseVisibilityService.isChangePositionVisible.next(false);
15143
16345
  }
15144
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 });
15145
- 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" }] });
15146
16348
  }
15147
16349
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NavigationBarComponent, decorators: [{
15148
16350
  type: Component,
15149
- 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"] }]
15150
16352
  }], ctorParameters: () => [{ type: undefined, decorators: [{
15151
16353
  type: Inject,
15152
16354
  args: ['currentAwsKinesisAnalyticsService']
@@ -15415,11 +16617,11 @@ class MenuBarComponent {
15415
16617
  }
15416
16618
  }
15417
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 });
15418
- 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" }] });
15419
16621
  }
15420
16622
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MenuBarComponent, decorators: [{
15421
16623
  type: Component,
15422
- 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"] }]
15423
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: [{
15424
16626
  type: Input
15425
16627
  }] } });
@@ -15574,27 +16776,27 @@ class BaseTagService {
15574
16776
  throw new Error('updatePoiMetadata is not implemented in the base class.');
15575
16777
  }*/
15576
16778
  async prepareEquipmentHtml(equip) {
15577
- throw new Error(`prepareEquipmentHtml is not implemented in the base class.
16779
+ throw new Error(`prepareEquipmentHtml is not implemented in the base class.
15578
16780
  Please override this method in your extended class.`);
15579
16781
  }
15580
16782
  async prepareTicketHtml(ticket) {
15581
- throw new Error(`prepareTicketHtml is not implemented in the base class.
16783
+ throw new Error(`prepareTicketHtml is not implemented in the base class.
15582
16784
  Please override this method in your extended class.`);
15583
16785
  }
15584
16786
  async prepareFeatureHtml(feature) {
15585
- throw new Error(`prepareFeatureHtml is not implemented in the base class.
16787
+ throw new Error(`prepareFeatureHtml is not implemented in the base class.
15586
16788
  Please override this method in your extended class.`);
15587
16789
  }
15588
16790
  async prepareMeasurementHtml(measure) {
15589
- throw new Error(`prepareMeasurementHtml is not implemented in the base class.
16791
+ throw new Error(`prepareMeasurementHtml is not implemented in the base class.
15590
16792
  Please override this method in your extended class.`);
15591
16793
  }
15592
16794
  async prepareDeskHtml(feature) {
15593
- throw new Error(`prepareDeskHtml is not implemented in the base class.
16795
+ throw new Error(`prepareDeskHtml is not implemented in the base class.
15594
16796
  Please override this method in your extended class.`);
15595
16797
  }
15596
16798
  async prepareIndicatorHtml(feature) {
15597
- throw new Error(`prepareDeskHtml is not implemented in the base class.
16799
+ throw new Error(`prepareDeskHtml is not implemented in the base class.
15598
16800
  Please override this method in your extended class.`);
15599
16801
  }
15600
16802
  async getSignedTagIconSource(tagIconSrc) {
@@ -15603,15 +16805,15 @@ class BaseTagService {
15603
16805
  getIconTagImageForFeature(feature, poi) {
15604
16806
  // const tagIcon = JSON.parse(poi.tagIcon);
15605
16807
  // return tagIcon.src;
15606
- throw new Error(`getIconTagImageForFeature is not implemented in the base class.
16808
+ throw new Error(`getIconTagImageForFeature is not implemented in the base class.
15607
16809
  Please override this method in your extended class.`);
15608
16810
  }
15609
16811
  getScriptForTag(object, tagType) {
15610
- throw new Error(`getScriptForTag is not implemented in the base class.
16812
+ throw new Error(`getScriptForTag is not implemented in the base class.
15611
16813
  Please override this method in your extended class.`);
15612
16814
  }
15613
16815
  getAnnexeForCommentTypeInFeature(feature, commentType) {
15614
- throw new Error(`getAnnexeForCommentTypeInFeature is not implemented in the base class.
16816
+ throw new Error(`getAnnexeForCommentTypeInFeature is not implemented in the base class.
15615
16817
  Please override this method in your extended class.`);
15616
16818
  }
15617
16819
  getBillboardMediaToEmbed(object) {
@@ -15620,27 +16822,27 @@ class BaseTagService {
15620
16822
  return { comment: null, tagDescription: '' };
15621
16823
  }
15622
16824
  onActionDetailClick(url) {
15623
- throw new Error(`onActionDetailClick is not implemented in the base class.
16825
+ throw new Error(`onActionDetailClick is not implemented in the base class.
15624
16826
  Please override this method in your extended class.`);
15625
16827
  }
15626
16828
  onActionAudioClick(audioCommentID) {
15627
- throw new Error(`onActionDetailClick is not implemented in the base class.
16829
+ throw new Error(`onActionDetailClick is not implemented in the base class.
15628
16830
  Please override this method in your extended class.`);
15629
16831
  }
15630
16832
  onActionVideoClick(url) {
15631
- throw new Error(`onActionVideoClick is not implemented in the base class.
16833
+ throw new Error(`onActionVideoClick is not implemented in the base class.
15632
16834
  Please override this method in your extended class.`);
15633
16835
  }
15634
16836
  onActionImageClick(imageCommentID) {
15635
- throw new Error(`onActionImageClick is not implemented in the base class.
16837
+ throw new Error(`onActionImageClick is not implemented in the base class.
15636
16838
  Please override this method in your extended class.`);
15637
16839
  }
15638
16840
  onActionDocClick(url) {
15639
- throw new Error(`onActionDocClick is not implemented in the base class.
16841
+ throw new Error(`onActionDocClick is not implemented in the base class.
15640
16842
  Please override this method in your extended class.`);
15641
16843
  }
15642
16844
  onActionYoutubeClick(url) {
15643
- throw new Error(`onActionYoutubeClick is not implemented in the base class.
16845
+ throw new Error(`onActionYoutubeClick is not implemented in the base class.
15644
16846
  Please override this method in your extended class.`);
15645
16847
  }
15646
16848
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BaseTagService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
@@ -19285,5 +20487,5 @@ function floatValidator() {
19285
20487
  * Generated bundle index. Do not edit.
19286
20488
  */
19287
20489
 
19288
- 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 };
19289
20491
  //# sourceMappingURL=smarterplan-ngx-smarterplan-core.mjs.map