@smarterplan/ngx-smarterplan-core 1.4.10 → 1.4.12
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.
- package/esm2022/lib/services/matterport-import.service.mjs +13 -62
- package/esm2022/lib/services/matterport-navigation.service.mjs +131 -1
- package/esm2022/lib/services/matterport-tag-copy.service.mjs +865 -0
- package/esm2022/lib/services/matterport-tag.service.mjs +13 -1
- package/esm2022/lib/services/matterport.service.mjs +55 -4
- package/esm2022/lib/services/models/equipment.service.mjs +16 -1
- package/esm2022/lib/services/models/navigation.service.mjs +4 -1
- package/esm2022/lib/services/models/poi.service.mjs +86 -8
- package/esm2022/lib/services/models/ticket.service.mjs +3 -2
- package/esm2022/lib/services/s3.service.mjs +12 -1
- package/esm2022/lib/services/utils/matterport-transform.util.mjs +39 -0
- package/esm2022/lib/services/viewer.service.mjs +12 -6
- package/esm2022/lib/tokens/matterport-sdk-key.token.mjs +8 -0
- package/esm2022/public-api.mjs +6 -1
- package/fesm2022/smarterplan-ngx-smarterplan-core.mjs +1227 -78
- package/fesm2022/smarterplan-ngx-smarterplan-core.mjs.map +1 -1
- package/lib/services/matterport-import.service.d.ts +2 -1
- package/lib/services/matterport-navigation.service.d.ts +25 -0
- package/lib/services/matterport-tag-copy.service.d.ts +129 -0
- package/lib/services/matterport.service.d.ts +27 -1
- package/lib/services/models/equipment.service.d.ts +8 -0
- package/lib/services/models/poi.service.d.ts +14 -0
- package/lib/services/s3.service.d.ts +8 -0
- package/lib/services/utils/matterport-transform.util.d.ts +21 -0
- package/lib/services/viewer.service.d.ts +2 -1
- package/lib/tokens/matterport-sdk-key.token.d.ts +7 -0
- package/package.json +1 -1
- package/public-api.d.ts +3 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, Input, Component, Inject, EventEmitter, isDevMode, Output, Pipe, ViewChild, NgModule } from '@angular/core';
|
|
2
|
+
import { Injectable, Input, Component, InjectionToken, Inject, EventEmitter, isDevMode, Output, Pipe, ViewChild, NgModule } from '@angular/core';
|
|
3
3
|
import * as i1 from '@ngx-translate/core';
|
|
4
4
|
import { TranslateModule } from '@ngx-translate/core';
|
|
5
5
|
import { Subject, BehaviorSubject, takeUntil, Observable } from 'rxjs';
|
|
@@ -9,8 +9,8 @@ import * as i1$1 from '@angular/router';
|
|
|
9
9
|
import { RouterModule } from '@angular/router';
|
|
10
10
|
import { Matrix4, Vector3, Color, Object3D, Euler, PerspectiveCamera, CylinderBufferGeometry, BoxGeometry, MeshBasicMaterial, NormalBlending, BackSide, Mesh, EdgesGeometry, AdditiveBlending, ShaderMaterial, AnimationMixer, VectorKeyframeTrack, InterpolateSmooth, AnimationClip, LoopOnce, Group, PlaneBufferGeometry, CanvasTexture, Texture, LinearFilter } from 'three';
|
|
11
11
|
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
12
|
+
import { Auth, API, graphqlOperation, Storage, Cache } from 'aws-amplify';
|
|
12
13
|
import { DateTime } from 'luxon';
|
|
13
|
-
import { Auth, Storage, API, Cache } from 'aws-amplify';
|
|
14
14
|
import * as i11 from '@angular/common';
|
|
15
15
|
import { CommonModule } from '@angular/common';
|
|
16
16
|
import * as FileSaver from 'file-saver';
|
|
@@ -96,6 +96,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
96
96
|
type: Input
|
|
97
97
|
}] } });
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Injection token holding the Matterport SDK application key.
|
|
101
|
+
* Each consuming app must provide its own value via its environment file
|
|
102
|
+
* (see app.module.ts of each app for the provider registration).
|
|
103
|
+
*/
|
|
104
|
+
const MATTERPORT_SDK_KEY = new InjectionToken('MATTERPORT_SDK_KEY');
|
|
105
|
+
|
|
99
106
|
var CameraMode;
|
|
100
107
|
(function (CameraMode) {
|
|
101
108
|
CameraMode["DOLLHOUSE"] = "mode.dollhouse";
|
|
@@ -1894,14 +1901,14 @@ class PoiService {
|
|
|
1894
1901
|
});
|
|
1895
1902
|
}
|
|
1896
1903
|
async getPoiByElementId(elementId) {
|
|
1897
|
-
const pois = await this.API.
|
|
1904
|
+
const pois = await this.API.PoisByElement(elementId);
|
|
1898
1905
|
if (pois && pois.items.length > 0) {
|
|
1899
1906
|
return pois.items[0];
|
|
1900
1907
|
}
|
|
1901
1908
|
return null;
|
|
1902
1909
|
}
|
|
1903
1910
|
async getPoisByType(type) {
|
|
1904
|
-
return this.API.
|
|
1911
|
+
return this.API.PoisByType(type).then((pois) => pois.items);
|
|
1905
1912
|
}
|
|
1906
1913
|
async createPOIForSpaceID(poi) {
|
|
1907
1914
|
removeNullKeysFromObject(poi);
|
|
@@ -1923,23 +1930,100 @@ class PoiService {
|
|
|
1923
1930
|
// partitionKey: "",
|
|
1924
1931
|
}, 'AWSKinesis');
|
|
1925
1932
|
}
|
|
1926
|
-
return this.API.
|
|
1933
|
+
return this.API.CreatePOI(poi);
|
|
1927
1934
|
}
|
|
1928
1935
|
poiIsVirtual(poi) {
|
|
1929
1936
|
const { coordinate } = poi;
|
|
1930
1937
|
return !coordinate;
|
|
1931
1938
|
}
|
|
1932
1939
|
async updatePoi(poi) {
|
|
1933
|
-
return this.API.
|
|
1940
|
+
return this.API.UpdatePOI(poi);
|
|
1934
1941
|
}
|
|
1935
1942
|
async deletePoi(poiID) {
|
|
1936
|
-
return this.API.
|
|
1943
|
+
return this.API.DeletePOI({ id: poiID });
|
|
1937
1944
|
}
|
|
1938
1945
|
async getPoisByMatterportSweepByType(sweep, type) {
|
|
1939
|
-
return this.API.
|
|
1940
|
-
.ListPOIs({ matterportSweepID: { eq: sweep } }, 300)
|
|
1946
|
+
return this.API.ListPOIS({ matterportSweepID: { eq: sweep } }, 300)
|
|
1941
1947
|
.then((response) => response.items.filter((poi) => poi.type === type));
|
|
1942
1948
|
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Optimized version: fetches POIs for MANY sweeps in as few GraphQL calls as possible,
|
|
1951
|
+
* using an `or: [{ matterportSweepID: { eq } }, ...]` filter instead of looping one
|
|
1952
|
+
* request per sweep. This is a DynamoDB Scan with a server-side filter (not a GSI
|
|
1953
|
+
* hash-key query), so `or` composition works and collapses N sweep-by-sweep calls
|
|
1954
|
+
* into a handful of paginated calls total.
|
|
1955
|
+
*
|
|
1956
|
+
* NOTE: `in` is NOT supported on this schema's ModelStringInput (older Amplify codegen),
|
|
1957
|
+
* hence the `or` composition instead — functionally equivalent for this use case.
|
|
1958
|
+
*
|
|
1959
|
+
* Chunked because DynamoDB/AppSync filter expressions have a practical size ceiling —
|
|
1960
|
+
* splitting keeps each request well within safe limits regardless of space size.
|
|
1961
|
+
*/
|
|
1962
|
+
async getPoisByMatterportSweeps(sweeps) {
|
|
1963
|
+
if (!sweeps || sweeps.length === 0) {
|
|
1964
|
+
return [];
|
|
1965
|
+
}
|
|
1966
|
+
const queryStatement = `
|
|
1967
|
+
query ListPOISLight($filter: ModelPOIFilterInput, $limit: Int, $nextToken: String) {
|
|
1968
|
+
listPOIS(filter: $filter, limit: $limit, nextToken: $nextToken) {
|
|
1969
|
+
items {
|
|
1970
|
+
id
|
|
1971
|
+
coordinate
|
|
1972
|
+
specific
|
|
1973
|
+
private
|
|
1974
|
+
global
|
|
1975
|
+
type
|
|
1976
|
+
elementID
|
|
1977
|
+
level
|
|
1978
|
+
role
|
|
1979
|
+
category
|
|
1980
|
+
domainID
|
|
1981
|
+
metadata
|
|
1982
|
+
tagIcon
|
|
1983
|
+
tagDescription
|
|
1984
|
+
tagImageID
|
|
1985
|
+
matterportSweepID
|
|
1986
|
+
createdAt
|
|
1987
|
+
updatedAt
|
|
1988
|
+
}
|
|
1989
|
+
nextToken
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
`;
|
|
1993
|
+
const CHUNK_SIZE = 50; // safe margin for an "or" filter expression's practical size
|
|
1994
|
+
const chunks = [];
|
|
1995
|
+
for (let i = 0; i < sweeps.length; i += CHUNK_SIZE) {
|
|
1996
|
+
chunks.push(sweeps.slice(i, i + CHUNK_SIZE));
|
|
1997
|
+
}
|
|
1998
|
+
let allPois = [];
|
|
1999
|
+
for (const chunk of chunks) {
|
|
2000
|
+
let nextToken = null;
|
|
2001
|
+
do {
|
|
2002
|
+
const variables = {
|
|
2003
|
+
filter: {
|
|
2004
|
+
or: chunk.map((sweep) => ({ matterportSweepID: { eq: sweep } }))
|
|
2005
|
+
},
|
|
2006
|
+
limit: 1000,
|
|
2007
|
+
nextToken
|
|
2008
|
+
};
|
|
2009
|
+
try {
|
|
2010
|
+
const response = (await API.graphql(graphqlOperation(queryStatement, variables)));
|
|
2011
|
+
const resData = response.data.listPOIS;
|
|
2012
|
+
if (resData && resData.items.length > 0) {
|
|
2013
|
+
allPois = [...allPois, ...resData.items];
|
|
2014
|
+
}
|
|
2015
|
+
nextToken = resData?.nextToken ?? null;
|
|
2016
|
+
}
|
|
2017
|
+
catch (error) {
|
|
2018
|
+
// A failed page shouldn't abort the whole batch — log and stop paging this chunk only,
|
|
2019
|
+
// the rest of the copy process continues with whatever was already collected.
|
|
2020
|
+
console.error(`Error while fetching POIs for a sweep chunk:`, error);
|
|
2021
|
+
nextToken = null;
|
|
2022
|
+
}
|
|
2023
|
+
} while (nextToken);
|
|
2024
|
+
}
|
|
2025
|
+
return allPois;
|
|
2026
|
+
}
|
|
1943
2027
|
unsubscribe() {
|
|
1944
2028
|
this.destroy$.next(true);
|
|
1945
2029
|
this.destroy$.complete();
|
|
@@ -2360,6 +2444,9 @@ class MatterportNavigationService {
|
|
|
2360
2444
|
else {
|
|
2361
2445
|
console.warn('No matterport floor found to move to');
|
|
2362
2446
|
}
|
|
2447
|
+
console.log("action_go_to_floor.floorMatterport", floorMatterport);
|
|
2448
|
+
console.log("action_go_to_floor.floorName", floorName);
|
|
2449
|
+
console.log("action_go_to_floor.matterportFloorSequence", matterportFloorSequence);
|
|
2363
2450
|
}
|
|
2364
2451
|
/**
|
|
2365
2452
|
* Navigate to a sweep. The caller can pass either a sid or a uuid;
|
|
@@ -2390,6 +2477,9 @@ class MatterportNavigationService {
|
|
|
2390
2477
|
await sdk.Camera.setRotation(rotation, { speed: 100 });
|
|
2391
2478
|
}
|
|
2392
2479
|
}, 1000);
|
|
2480
|
+
console.log("action_go_to_sweep.sweep", sweep);
|
|
2481
|
+
console.log("action_go_to_sweep.rotation", rotation);
|
|
2482
|
+
console.log("action_go_to_sweep.resolvedSid", resolvedSid);
|
|
2393
2483
|
}
|
|
2394
2484
|
/**
|
|
2395
2485
|
* Disable forbidden sweeps. Resolves each sweep ID to sid before calling SDK.
|
|
@@ -2409,6 +2499,130 @@ class MatterportNavigationService {
|
|
|
2409
2499
|
}));
|
|
2410
2500
|
console.log('removed sweeps:', removed);
|
|
2411
2501
|
}
|
|
2502
|
+
/**
|
|
2503
|
+
* Move to a position
|
|
2504
|
+
* @param sdk
|
|
2505
|
+
* @param position
|
|
2506
|
+
* @param rotation
|
|
2507
|
+
* @param options
|
|
2508
|
+
*/
|
|
2509
|
+
async move_to_position(sdk, position, rotation, options) {
|
|
2510
|
+
// Validate SDK
|
|
2511
|
+
if (!sdk) {
|
|
2512
|
+
throw new Error('Matterport SDK instance is required');
|
|
2513
|
+
}
|
|
2514
|
+
// Merge options with defaults
|
|
2515
|
+
const { transition = 'FLY', durationMs = 800, anglesInDegrees = true, offset = { x: 0, y: 0, z: 0 } } = options || {};
|
|
2516
|
+
const degToRad = (d) => d * Math.PI / 180;
|
|
2517
|
+
// Prepare rotation values (default to 0 when not provided)
|
|
2518
|
+
const yaw = rotation?.yaw ?? 0;
|
|
2519
|
+
const pitch = rotation?.pitch ?? 0;
|
|
2520
|
+
const roll = rotation?.roll ?? 0;
|
|
2521
|
+
// Convert to radians if angles are provided in degrees
|
|
2522
|
+
const rotYaw = anglesInDegrees ? degToRad(yaw) : yaw;
|
|
2523
|
+
const rotPitch = anglesInDegrees ? degToRad(pitch) : pitch;
|
|
2524
|
+
const rotRoll = anglesInDegrees ? degToRad(roll) : roll;
|
|
2525
|
+
// Preserve previous transition flag and mark as in-transition
|
|
2526
|
+
const prevInTransition = this.inTransitionMode;
|
|
2527
|
+
this.inTransitionMode = true;
|
|
2528
|
+
try {
|
|
2529
|
+
// Attempt to use Camera.lookAt when available
|
|
2530
|
+
if (sdk?.Camera?.lookAt) {
|
|
2531
|
+
const lookAtOpts = {};
|
|
2532
|
+
// Prefer SDK constants when available, otherwise use fallback string
|
|
2533
|
+
if (sdk.Camera.TransitionType && sdk.Camera.TransitionType[transition]) {
|
|
2534
|
+
lookAtOpts.transition = sdk.Camera.TransitionType[transition];
|
|
2535
|
+
}
|
|
2536
|
+
else {
|
|
2537
|
+
lookAtOpts.transition = transition;
|
|
2538
|
+
}
|
|
2539
|
+
lookAtOpts.duration = durationMs;
|
|
2540
|
+
if (offset)
|
|
2541
|
+
lookAtOpts.offset = offset;
|
|
2542
|
+
try {
|
|
2543
|
+
await sdk.Camera.lookAt(position, lookAtOpts);
|
|
2544
|
+
}
|
|
2545
|
+
catch (err) {
|
|
2546
|
+
// If lookAt fails, continue to fallbacks below
|
|
2547
|
+
if (isDevMode())
|
|
2548
|
+
console.debug('Camera.lookAt failed, falling back to setState', err);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
// If lookAt is not present or failed, try setState for position
|
|
2552
|
+
if (!sdk?.Camera?.lookAt && sdk?.Camera?.setState) {
|
|
2553
|
+
const statePos = {
|
|
2554
|
+
position: { x: position.x, y: position.y, z: position.z }
|
|
2555
|
+
};
|
|
2556
|
+
try {
|
|
2557
|
+
await sdk.Camera.setState(statePos);
|
|
2558
|
+
}
|
|
2559
|
+
catch (err) {
|
|
2560
|
+
if (isDevMode())
|
|
2561
|
+
console.debug('Camera.setState(position) failed', err);
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
// Apply rotation if provided
|
|
2565
|
+
if (rotation) {
|
|
2566
|
+
// Prefer setRotation when available
|
|
2567
|
+
if (sdk?.Camera?.setRotation) {
|
|
2568
|
+
// Try Vector2 signature { x: yaw, y: pitch } first
|
|
2569
|
+
try {
|
|
2570
|
+
await sdk.Camera.setRotation({ x: rotYaw, y: rotPitch }, { speed: Math.max(10, Math.floor(durationMs / 10)) });
|
|
2571
|
+
}
|
|
2572
|
+
catch (err1) {
|
|
2573
|
+
// If that fails, try Vector3 signature { x: pitch, y: yaw, z: roll }
|
|
2574
|
+
try {
|
|
2575
|
+
await sdk.Camera.setRotation({ x: rotPitch, y: rotYaw, z: rotRoll }, { speed: Math.max(10, Math.floor(durationMs / 10)) });
|
|
2576
|
+
}
|
|
2577
|
+
catch (err2) {
|
|
2578
|
+
if (isDevMode())
|
|
2579
|
+
console.debug('Camera.setRotation failed with multiple signatures', err1, err2);
|
|
2580
|
+
// Fallback to setState with rotation if available
|
|
2581
|
+
if (sdk?.Camera?.setState) {
|
|
2582
|
+
try {
|
|
2583
|
+
await sdk.Camera.setState({
|
|
2584
|
+
rotation: { x: rotPitch, y: rotYaw, z: rotRoll },
|
|
2585
|
+
position: { x: position.x, y: position.y, z: position.z }
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
catch (err3) {
|
|
2589
|
+
if (isDevMode())
|
|
2590
|
+
console.debug('Fallback Camera.setState(rotation) failed', err3);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
else if (sdk?.Camera?.setState) {
|
|
2597
|
+
// If setRotation is not available, use setState to apply rotation
|
|
2598
|
+
try {
|
|
2599
|
+
await sdk.Camera.setState({
|
|
2600
|
+
rotation: { x: rotPitch, y: rotYaw, z: rotRoll },
|
|
2601
|
+
position: { x: position.x, y: position.y, z: position.z }
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
catch (err) {
|
|
2605
|
+
if (isDevMode())
|
|
2606
|
+
console.debug('Camera.setState(rotation) failed', err);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
else {
|
|
2610
|
+
if (isDevMode())
|
|
2611
|
+
console.warn('No method available to apply rotation (setRotation/setState not found)');
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
// Wait a bit to allow transition animations to complete
|
|
2615
|
+
await wait(durationMs + 120);
|
|
2616
|
+
}
|
|
2617
|
+
catch (err) {
|
|
2618
|
+
// Re-throw after ensuring flags are restored in finally
|
|
2619
|
+
throw err;
|
|
2620
|
+
}
|
|
2621
|
+
finally {
|
|
2622
|
+
// Restore previous transition flag
|
|
2623
|
+
this.inTransitionMode = prevInTransition;
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2412
2626
|
/**
|
|
2413
2627
|
* Returns the current sweep as a **uuid** (for database matching).
|
|
2414
2628
|
* The SDK's poseCamera.sweep is a sid; we translate it.
|
|
@@ -3236,6 +3450,12 @@ class MatterportTagService {
|
|
|
3236
3450
|
async addMattertagToViewer(sdk, mattertagData) {
|
|
3237
3451
|
if (!sdk)
|
|
3238
3452
|
return null;
|
|
3453
|
+
console.log('---------------');
|
|
3454
|
+
console.log('element id ' + mattertagData.elementID);
|
|
3455
|
+
console.log('position: ', mattertagData.getPosition());
|
|
3456
|
+
console.log('rotation: ', mattertagData.getRotation());
|
|
3457
|
+
console.log('sweep id: ', mattertagData.getSweepID());
|
|
3458
|
+
console.log('---------------');
|
|
3239
3459
|
this.tagAddQueue = this.tagAddQueue.then(async () => {
|
|
3240
3460
|
const data = mattertagData.getData();
|
|
3241
3461
|
const existingId = data?.id || null;
|
|
@@ -3489,6 +3709,12 @@ class MatterportTagService {
|
|
|
3489
3709
|
}
|
|
3490
3710
|
mattertagData.setPoi(poi);
|
|
3491
3711
|
const createdTagID = await this.addMattertagToViewer(sdk, mattertagData);
|
|
3712
|
+
console.log('---------------');
|
|
3713
|
+
console.log('element id ' + mattertagData.elementID);
|
|
3714
|
+
console.log('position: ', mattertagData.getPosition());
|
|
3715
|
+
console.log('rotation: ', mattertagData.getRotation());
|
|
3716
|
+
console.log('sweep id: ', mattertagData.getSweepID());
|
|
3717
|
+
console.log('---------------');
|
|
3492
3718
|
if (createdTagID && sdk) {
|
|
3493
3719
|
await wait(100);
|
|
3494
3720
|
await setTagIconAndOpacity(createdTagID, mattertagData);
|
|
@@ -3663,6 +3889,7 @@ class Config {
|
|
|
3663
3889
|
}
|
|
3664
3890
|
|
|
3665
3891
|
class MatterportService {
|
|
3892
|
+
sdkKey;
|
|
3666
3893
|
router;
|
|
3667
3894
|
activeRoute;
|
|
3668
3895
|
visibilityService;
|
|
@@ -3826,7 +4053,8 @@ class MatterportService {
|
|
|
3826
4053
|
pointerMiddleClickHandler = (e) => {
|
|
3827
4054
|
this.pointerService.updateCursorDisplay(this.poseMatterport);
|
|
3828
4055
|
};
|
|
3829
|
-
constructor(config, router, activeRoute, visibilityService, ngZone, measurementService, navigationService, pointerService, object3DService, matterportTagService) {
|
|
4056
|
+
constructor(config, sdkKey, router, activeRoute, visibilityService, ngZone, measurementService, navigationService, pointerService, object3DService, matterportTagService) {
|
|
4057
|
+
this.sdkKey = sdkKey;
|
|
3830
4058
|
this.router = router;
|
|
3831
4059
|
this.activeRoute = activeRoute;
|
|
3832
4060
|
this.visibilityService = visibilityService;
|
|
@@ -3871,7 +4099,7 @@ class MatterportService {
|
|
|
3871
4099
|
const showcaseWindow = iframe.contentWindow;
|
|
3872
4100
|
iframe.addEventListener('load', async () => {
|
|
3873
4101
|
try {
|
|
3874
|
-
this.sdk = await showcaseWindow.MP_SDK.connect(showcaseWindow,
|
|
4102
|
+
this.sdk = await showcaseWindow.MP_SDK.connect(showcaseWindow, this.sdkKey, '');
|
|
3875
4103
|
}
|
|
3876
4104
|
catch (e) {
|
|
3877
4105
|
console.error(e);
|
|
@@ -4329,6 +4557,51 @@ class MatterportService {
|
|
|
4329
4557
|
console.warn('Cannot navigate to tag', error);
|
|
4330
4558
|
}
|
|
4331
4559
|
}
|
|
4560
|
+
/**
|
|
4561
|
+
* Move the viewer camera to a specific 3D position and apply a rotation.
|
|
4562
|
+
* Delegates the heavy lifting to MatterportNavigationService.move_to_position.
|
|
4563
|
+
* - position: { x, y, z }
|
|
4564
|
+
* - rotation: { yaw, pitch?, roll? } in degrees by default
|
|
4565
|
+
* - options: optional transition and conversion settings (see navigation service)
|
|
4566
|
+
*/
|
|
4567
|
+
async goToPosition(position, rotation = null, options) {
|
|
4568
|
+
// Ensure SDK is initialized
|
|
4569
|
+
if (!this.sdk) {
|
|
4570
|
+
throw new Error('Matterport SDK is not initialized');
|
|
4571
|
+
}
|
|
4572
|
+
// Prevent concurrent navigation if already in a sweep/mode transition
|
|
4573
|
+
if (this.inTransitionMode || this.inTransitionSweep) {
|
|
4574
|
+
if (isDevMode())
|
|
4575
|
+
console.debug('goToPosition aborted: viewer is in transition');
|
|
4576
|
+
return;
|
|
4577
|
+
}
|
|
4578
|
+
try {
|
|
4579
|
+
// Mark transition state on navigation service (move_to_position will also manage flags)
|
|
4580
|
+
this.inTransitionMode = true;
|
|
4581
|
+
// Delegate to navigation service which contains the robust move_to_position implementation
|
|
4582
|
+
await this.navigationService.move_to_position(this.sdk, position, rotation, options);
|
|
4583
|
+
// Update local cached camera info
|
|
4584
|
+
this.lastCameraPosition = { ...position };
|
|
4585
|
+
// Emit a simplified current camera pose for subscribers
|
|
4586
|
+
this.currentCameraPose.next({
|
|
4587
|
+
position: { ...position },
|
|
4588
|
+
rotation: rotation ? { ...rotation } : null,
|
|
4589
|
+
sweep: this.poseCamera?.sweep ?? null
|
|
4590
|
+
});
|
|
4591
|
+
if (isDevMode()) {
|
|
4592
|
+
console.debug('goToPosition completed', { position, rotation, options });
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
catch (err) {
|
|
4596
|
+
if (isDevMode())
|
|
4597
|
+
console.error('goToPosition error', err);
|
|
4598
|
+
throw err;
|
|
4599
|
+
}
|
|
4600
|
+
finally {
|
|
4601
|
+
// Ensure transition flag is cleared (navigation service may also clear it)
|
|
4602
|
+
this.inTransitionMode = false;
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4332
4605
|
/**
|
|
4333
4606
|
* Computes the context-aware opacity for a single tag at the moment it is rendered.
|
|
4334
4607
|
* - Cursor tag: always use provided defaultOpacity.
|
|
@@ -4780,7 +5053,7 @@ class MatterportService {
|
|
|
4780
5053
|
toggleViewFrustum() {
|
|
4781
5054
|
this.object3DService.toggleViewFrustum();
|
|
4782
5055
|
}
|
|
4783
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportService, deps: [{ token: 'config' }, { token: i1$1.Router }, { token: i1$1.ActivatedRoute }, { token: BaseVisibilityService }, { token: i0.NgZone }, { token: MatterportMeasurementService }, { token: MatterportNavigationService }, { token: MatterportPointerService }, { token: MatterportObject3DService }, { token: MatterportTagService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5056
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportService, deps: [{ token: 'config' }, { token: MATTERPORT_SDK_KEY }, { token: i1$1.Router }, { token: i1$1.ActivatedRoute }, { token: BaseVisibilityService }, { token: i0.NgZone }, { token: MatterportMeasurementService }, { token: MatterportNavigationService }, { token: MatterportPointerService }, { token: MatterportObject3DService }, { token: MatterportTagService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4784
5057
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportService, providedIn: 'root' });
|
|
4785
5058
|
}
|
|
4786
5059
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportService, decorators: [{
|
|
@@ -4791,6 +5064,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
4791
5064
|
}], ctorParameters: () => [{ type: Config, decorators: [{
|
|
4792
5065
|
type: Inject,
|
|
4793
5066
|
args: ['config']
|
|
5067
|
+
}] }, { type: undefined, decorators: [{
|
|
5068
|
+
type: Inject,
|
|
5069
|
+
args: [MATTERPORT_SDK_KEY]
|
|
4794
5070
|
}] }, { type: i1$1.Router }, { type: i1$1.ActivatedRoute }, { type: BaseVisibilityService }, { type: i0.NgZone }, { type: MatterportMeasurementService }, { type: MatterportNavigationService }, { type: MatterportPointerService }, { type: MatterportObject3DService }, { type: MatterportTagService }] });
|
|
4795
5071
|
|
|
4796
5072
|
/* eslint-disable no-await-in-loop */
|
|
@@ -4798,6 +5074,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
4798
5074
|
/* eslint-disable no-underscore-dangle */
|
|
4799
5075
|
/* eslint-disable class-methods-use-this */
|
|
4800
5076
|
class ViewerService {
|
|
5077
|
+
sdkKey;
|
|
4801
5078
|
poiService;
|
|
4802
5079
|
matterportService;
|
|
4803
5080
|
viewerMode = 'Matterport';
|
|
@@ -4835,7 +5112,8 @@ class ViewerService {
|
|
|
4835
5112
|
},
|
|
4836
5113
|
};
|
|
4837
5114
|
isObjectLoaded = false;
|
|
4838
|
-
constructor(poiService, matterportService) {
|
|
5115
|
+
constructor(sdkKey, poiService, matterportService) {
|
|
5116
|
+
this.sdkKey = sdkKey;
|
|
4839
5117
|
this.poiService = poiService;
|
|
4840
5118
|
this.matterportService = matterportService;
|
|
4841
5119
|
this.setMode('Matterport');
|
|
@@ -4865,7 +5143,7 @@ class ViewerService {
|
|
|
4865
5143
|
params.push(`log=0`);
|
|
4866
5144
|
params.push(`brand=0`);
|
|
4867
5145
|
params.push(`search=0`);
|
|
4868
|
-
params.push(`applicationKey
|
|
5146
|
+
params.push(`applicationKey=${this.sdkKey}`);
|
|
4869
5147
|
params.push(`mds=0`);
|
|
4870
5148
|
// Quick start
|
|
4871
5149
|
if (showQuickStart) {
|
|
@@ -5215,7 +5493,7 @@ class ViewerService {
|
|
|
5215
5493
|
isObject3DLoadedInPosition() {
|
|
5216
5494
|
return this.isObjectLoaded;
|
|
5217
5495
|
}
|
|
5218
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, deps: [{ token: PoiService }, { token: MatterportService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5496
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, deps: [{ token: MATTERPORT_SDK_KEY }, { token: PoiService }, { token: MatterportService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5219
5497
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, providedIn: 'root' });
|
|
5220
5498
|
}
|
|
5221
5499
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ViewerService, decorators: [{
|
|
@@ -5223,7 +5501,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
5223
5501
|
args: [{
|
|
5224
5502
|
providedIn: 'root',
|
|
5225
5503
|
}]
|
|
5226
|
-
}], ctorParameters: () => [{ type:
|
|
5504
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
5505
|
+
type: Inject,
|
|
5506
|
+
args: [MATTERPORT_SDK_KEY]
|
|
5507
|
+
}] }, { type: PoiService }, { type: MatterportService }] });
|
|
5227
5508
|
|
|
5228
5509
|
class ModalSwitchVisitComponent {
|
|
5229
5510
|
activeModal;
|
|
@@ -5401,6 +5682,17 @@ const uploadJsonToS3 = async (path, json, filename) => {
|
|
|
5401
5682
|
return null;
|
|
5402
5683
|
});
|
|
5403
5684
|
};
|
|
5685
|
+
/**
|
|
5686
|
+
* Physically duplicates an S3 object to a new key. Used when copying attachments
|
|
5687
|
+
* between features/tickets/etc. so the copy is fully independent: deleting the
|
|
5688
|
+
* original file later must not affect the copy.
|
|
5689
|
+
* IMPORTANT: does NOT swallow errors — callers need the real error (e.g. NoSuchKey)
|
|
5690
|
+
* to distinguish an orphaned DB reference from a genuine copy failure.
|
|
5691
|
+
*/
|
|
5692
|
+
const copyFileInS3 = async (sourceKey, destKey) => {
|
|
5693
|
+
const result = await Storage.copy({ key: sourceKey, level: "public" }, { key: destKey, level: "public" });
|
|
5694
|
+
return result.key;
|
|
5695
|
+
};
|
|
5404
5696
|
|
|
5405
5697
|
function getDistanceBetweenTwoPoints(startPoint, endPoint) {
|
|
5406
5698
|
const dx = endPoint.x - startPoint.x;
|
|
@@ -6742,13 +7034,16 @@ class NavigationService {
|
|
|
6742
7034
|
}
|
|
6743
7035
|
async getAllowedSweepsIDsForUserInSpace(spaceID) {
|
|
6744
7036
|
const missionsForSpace = this.userService.currentMissions.filter((mis) => mis.spaceID === spaceID);
|
|
7037
|
+
console.log(missionsForSpace);
|
|
6745
7038
|
let navIDs = [];
|
|
6746
7039
|
if (this.userService.userHasAccessToAllZonesInSpace(spaceID)) {
|
|
7040
|
+
console.log(1);
|
|
6747
7041
|
return this.getSweepsIDsForSpace(spaceID);
|
|
6748
7042
|
}
|
|
6749
7043
|
await Promise.all(missionsForSpace.map(async (mission) => {
|
|
6750
7044
|
navIDs = [...navIDs, ...mission.zone.sweepIDs];
|
|
6751
7045
|
}));
|
|
7046
|
+
console.log('final navids = ', navIDs);
|
|
6752
7047
|
return navIDs;
|
|
6753
7048
|
}
|
|
6754
7049
|
async getForbiddenNavigationIDsForUserInSpace(spaceID) {
|
|
@@ -7916,7 +8211,8 @@ class TicketsService extends BaseObjectService {
|
|
|
7916
8211
|
// partitionKey: "",
|
|
7917
8212
|
}, 'AWSKinesis');
|
|
7918
8213
|
}
|
|
7919
|
-
|
|
8214
|
+
const { id, createdAt, updatedAt, __typename, children, domain, ownerMission, equipments, prestataire, space, pois, ...graphqlInput } = input;
|
|
8215
|
+
return this.API.CreateTicket(input);
|
|
7920
8216
|
}
|
|
7921
8217
|
async updateTicket(input) {
|
|
7922
8218
|
return this.API.__proto__.UpdateTicket(input);
|
|
@@ -8298,6 +8594,21 @@ class EquipmentService extends BaseObjectService {
|
|
|
8298
8594
|
return [];
|
|
8299
8595
|
});
|
|
8300
8596
|
}
|
|
8597
|
+
/**
|
|
8598
|
+
* Creates a Note attached to an equipment from an S3 key that ALREADY exists
|
|
8599
|
+
* (as opposed to uploadDoc/uploadImage, which upload a fresh browser File).
|
|
8600
|
+
* Used when duplicating equipment attachments during a cross-visit copy: the
|
|
8601
|
+
* S3 object itself has already been physically copied by the caller beforehand,
|
|
8602
|
+
* so this only needs to record the Note pointing at that new key.
|
|
8603
|
+
*/
|
|
8604
|
+
async createNoteWithExistingAttachment(equipmentID, attachmentKey, type, ownerMissionID) {
|
|
8605
|
+
return this.API.__proto__.CreateNote({
|
|
8606
|
+
attachment: attachmentKey,
|
|
8607
|
+
type,
|
|
8608
|
+
equipmentID,
|
|
8609
|
+
ownerMissionID,
|
|
8610
|
+
});
|
|
8611
|
+
}
|
|
8301
8612
|
async uploadImage(equipment, imageFile) {
|
|
8302
8613
|
return uploadFileToS3(this.prefix.image, imageFile, equipment.id).then(async (url) => {
|
|
8303
8614
|
if (url) {
|
|
@@ -12612,6 +12923,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
12612
12923
|
}] }] });
|
|
12613
12924
|
|
|
12614
12925
|
class MatterportImportService {
|
|
12926
|
+
sdkKey;
|
|
12615
12927
|
navigationService;
|
|
12616
12928
|
zoneService;
|
|
12617
12929
|
viewerService;
|
|
@@ -12641,7 +12953,8 @@ class MatterportImportService {
|
|
|
12641
12953
|
importingImages = new Subject();
|
|
12642
12954
|
sweepProcessedCount = new Subject();
|
|
12643
12955
|
totalSweepsCount = new Subject();
|
|
12644
|
-
constructor(navigationService, zoneService, viewerService, layerService, userService, planService) {
|
|
12956
|
+
constructor(sdkKey, navigationService, zoneService, viewerService, layerService, userService, planService) {
|
|
12957
|
+
this.sdkKey = sdkKey;
|
|
12645
12958
|
this.navigationService = navigationService;
|
|
12646
12959
|
this.zoneService = zoneService;
|
|
12647
12960
|
this.viewerService = viewerService;
|
|
@@ -12672,34 +12985,27 @@ class MatterportImportService {
|
|
|
12672
12985
|
return new Promise((res, rej) => {
|
|
12673
12986
|
iframe.addEventListener('load', async () => {
|
|
12674
12987
|
try {
|
|
12675
|
-
this.sdk = await showcaseWindow.MP_SDK.connect(iframe,
|
|
12676
|
-
// Subscribe to Floor data
|
|
12988
|
+
this.sdk = await showcaseWindow.MP_SDK.connect(iframe, this.sdkKey, '');
|
|
12677
12989
|
this.sdk.Floor.data.subscribe({
|
|
12678
12990
|
onCollectionUpdated: function upd(collection) {
|
|
12679
|
-
// console.log("Floors", collection);
|
|
12680
12991
|
this.floors = collection;
|
|
12681
12992
|
}.bind(this),
|
|
12682
12993
|
});
|
|
12683
|
-
// Subscribe to current Floor
|
|
12684
12994
|
this.sdk.Floor.current.subscribe(function upd(currentFloor) {
|
|
12685
12995
|
this.currentFloor = currentFloor;
|
|
12686
12996
|
}.bind(this));
|
|
12687
|
-
// Subscribe to Sweep data
|
|
12688
12997
|
this.sdk.Sweep.data.subscribe({
|
|
12689
12998
|
onCollectionUpdated: function upd(collection) {
|
|
12690
12999
|
console.log('the entire up-to-date collection', collection);
|
|
12691
13000
|
this.sweeps = collection;
|
|
12692
13001
|
}.bind(this),
|
|
12693
13002
|
});
|
|
12694
|
-
// remove labels
|
|
12695
13003
|
await this.sdk.Settings.update('labels', false);
|
|
12696
13004
|
let timeoutExpired = false;
|
|
12697
|
-
// start timeout
|
|
12698
13005
|
setTimeout(() => (timeoutExpired = true), 7000);
|
|
12699
13006
|
await waitUntil(() => (this.sweeps !== undefined && this.floors !== undefined) ||
|
|
12700
13007
|
timeoutExpired === true);
|
|
12701
13008
|
if (timeoutExpired) {
|
|
12702
|
-
// we are likely in matterport error case (that we cannot catch!)
|
|
12703
13009
|
console.log('timeout intitialising Matterport');
|
|
12704
13010
|
return rej(new Error('error init Matterport'));
|
|
12705
13011
|
}
|
|
@@ -12719,7 +13025,6 @@ class MatterportImportService {
|
|
|
12719
13025
|
}
|
|
12720
13026
|
let appIsLoaded = false;
|
|
12721
13027
|
this.sdk.App.state.subscribe(function (appState) {
|
|
12722
|
-
// app state has changed
|
|
12723
13028
|
console.log('The current phase: ', appState.phase);
|
|
12724
13029
|
if (appState.phase === 'appphase.playing') {
|
|
12725
13030
|
console.log('The app is playing');
|
|
@@ -12728,7 +13033,6 @@ class MatterportImportService {
|
|
|
12728
13033
|
});
|
|
12729
13034
|
await waitUntil(() => appIsLoaded === true);
|
|
12730
13035
|
if (reloading) {
|
|
12731
|
-
// perform only 360 import since we are reloading
|
|
12732
13036
|
await this.import360images(overrideExisting);
|
|
12733
13037
|
return Promise.resolve(true);
|
|
12734
13038
|
}
|
|
@@ -12740,10 +13044,8 @@ class MatterportImportService {
|
|
|
12740
13044
|
return Promise.reject(new Error('missing data (no main Building Zone)'));
|
|
12741
13045
|
}
|
|
12742
13046
|
console.log('importing data from matterport');
|
|
12743
|
-
// save sweeps data as json
|
|
12744
13047
|
const json = JSON.stringify(Object.values(this.sweeps));
|
|
12745
13048
|
await uploadJsonToS3(`visits/${this.modelID}/inventory/`, json, 'data.json');
|
|
12746
|
-
// check layer of building type
|
|
12747
13049
|
const layerBuilding = await this.getLayerByName('BUILDING');
|
|
12748
13050
|
const createdDefaultZone = await this.createZone(node.metadata, layerBuilding.id, surface, mainZone.id, Object.keys(this.sweeps), node);
|
|
12749
13051
|
if (!createdDefaultZone) {
|
|
@@ -12752,7 +13054,6 @@ class MatterportImportService {
|
|
|
12752
13054
|
if (this.floors) {
|
|
12753
13055
|
floorZones = await this.importFloorData(createdDefaultZone.id, node);
|
|
12754
13056
|
}
|
|
12755
|
-
// console.log(floorZones);
|
|
12756
13057
|
await Promise.all(Object.keys(this.sweeps).map(async (key) => {
|
|
12757
13058
|
const sweepInfo = this.sweeps[key];
|
|
12758
13059
|
const navigationInput = {
|
|
@@ -12792,7 +13093,6 @@ class MatterportImportService {
|
|
|
12792
13093
|
const newLocal = await this.zoneService.create(zoneInput);
|
|
12793
13094
|
return newLocal;
|
|
12794
13095
|
}
|
|
12795
|
-
// --- Main import function (complete) ---
|
|
12796
13096
|
async import360images(overrideExisting = true) {
|
|
12797
13097
|
console.log('Importing 360 images');
|
|
12798
13098
|
this.importingImages.next(true);
|
|
@@ -12810,10 +13110,8 @@ class MatterportImportService {
|
|
|
12810
13110
|
}
|
|
12811
13111
|
const sweep = scans[index];
|
|
12812
13112
|
console.log('Moving to sweep:', sweep);
|
|
12813
|
-
// Prefer legacy .id first, then uuid, then sid as last resort
|
|
12814
13113
|
const moveId = sweep.id || sweep.uuid || sweep.sid;
|
|
12815
13114
|
try {
|
|
12816
|
-
// Move to sweep (use the best available id)
|
|
12817
13115
|
await this.sdk.Sweep.moveTo(moveId, {
|
|
12818
13116
|
rotation: { x: 0, y: 0 },
|
|
12819
13117
|
transition: this.sdk.Camera && this.sdk.Camera.TransitionType
|
|
@@ -12821,7 +13119,6 @@ class MatterportImportService {
|
|
|
12821
13119
|
: undefined,
|
|
12822
13120
|
transitionTime: 0,
|
|
12823
13121
|
});
|
|
12824
|
-
// Capture with automatic retry (ensures renderer readiness each attempt)
|
|
12825
13122
|
const img = await this.captureEquirectangularWithRetry({ width: 2048, height: 1024 }, { mattertags: false, sweeps: true }, {
|
|
12826
13123
|
maxAttempts: 3,
|
|
12827
13124
|
initialBackoffMs: 500,
|
|
@@ -12829,10 +13126,7 @@ class MatterportImportService {
|
|
|
12829
13126
|
sweepIdForReadyCheck: moveId,
|
|
12830
13127
|
waitForSweepTimeoutMs: 12000
|
|
12831
13128
|
});
|
|
12832
|
-
|
|
12833
|
-
uploadBase64ImageWithRetry(img,
|
|
12834
|
-
// prefer using uuid for storage name if available, else fallback to moveId
|
|
12835
|
-
sweep.uuid || moveId, `visits/${this.modelID}/sweeps/`, 'sweep', 5).then(() => {
|
|
13129
|
+
uploadBase64ImageWithRetry(img, sweep.uuid || moveId, `visits/${this.modelID}/sweeps/`, 'sweep', 5).then(() => {
|
|
12836
13130
|
this.sweepProcessedCount.next(index);
|
|
12837
13131
|
if (index === nmbScans - 1) {
|
|
12838
13132
|
console.log('Import 360 done');
|
|
@@ -12845,38 +13139,29 @@ class MatterportImportService {
|
|
|
12845
13139
|
});
|
|
12846
13140
|
}
|
|
12847
13141
|
catch (err) {
|
|
12848
|
-
// capture failed after retries — log and continue to next sweep
|
|
12849
13142
|
console.error(`Failed to capture sweep ${moveId} after retries:`, err);
|
|
12850
13143
|
this.sweepProcessedCount.next(index);
|
|
12851
|
-
// continue to next sweep (change to `throw err` if you want to abort on first failure)
|
|
12852
13144
|
continue;
|
|
12853
13145
|
}
|
|
12854
13146
|
}
|
|
12855
|
-
// If loop finishes without hitting the final resolve in upload promise:
|
|
12856
13147
|
resolve(true);
|
|
12857
13148
|
this.removeFrame();
|
|
12858
13149
|
});
|
|
12859
13150
|
}
|
|
12860
|
-
// --- capture with retry and backoff ---
|
|
12861
|
-
// options: renderer size; renderOptions: renderer flags; cfg: retry config
|
|
12862
13151
|
async captureEquirectangularWithRetry(options, renderOptions, { maxAttempts = 3, initialBackoffMs = 500, perAttemptTimeoutMs = 8000, sweepIdForReadyCheck = null, waitForSweepTimeoutMs = 12000 } = {}) {
|
|
12863
13152
|
let attempt = 0;
|
|
12864
13153
|
let lastError = null;
|
|
12865
13154
|
while (attempt < maxAttempts) {
|
|
12866
13155
|
attempt += 1;
|
|
12867
13156
|
try {
|
|
12868
|
-
// Ensure sweep is ready before each attempt if requested
|
|
12869
13157
|
if (sweepIdForReadyCheck) {
|
|
12870
13158
|
await this.waitForSweepReady(sweepIdForReadyCheck, waitForSweepTimeoutMs);
|
|
12871
|
-
// small safety delay to let renderer finalize
|
|
12872
13159
|
await this.sleep(120);
|
|
12873
13160
|
}
|
|
12874
|
-
// Race the takeEquirectangular call against a per-attempt timeout
|
|
12875
13161
|
const img = await Promise.race([
|
|
12876
13162
|
this.sdk.Renderer.takeEquirectangular(options, renderOptions),
|
|
12877
13163
|
this.timeoutPromise(perAttemptTimeoutMs, `takeEquirectangular timed out after ${perAttemptTimeoutMs}ms`)
|
|
12878
13164
|
]);
|
|
12879
|
-
// success
|
|
12880
13165
|
return img;
|
|
12881
13166
|
}
|
|
12882
13167
|
catch (err) {
|
|
@@ -12885,37 +13170,27 @@ class MatterportImportService {
|
|
|
12885
13170
|
if (attempt >= maxAttempts) {
|
|
12886
13171
|
throw lastError;
|
|
12887
13172
|
}
|
|
12888
|
-
// exponential backoff before next attempt
|
|
12889
13173
|
const backoff = initialBackoffMs * Math.pow(2, attempt - 1);
|
|
12890
|
-
// add small jitter to reduce thundering herd
|
|
12891
13174
|
const jitter = Math.floor(Math.random() * 100);
|
|
12892
13175
|
await this.sleep(backoff + jitter);
|
|
12893
|
-
// loop to next attempt
|
|
12894
13176
|
}
|
|
12895
13177
|
}
|
|
12896
13178
|
throw lastError || new Error('captureEquirectangularWithRetry failed without error');
|
|
12897
13179
|
}
|
|
12898
|
-
// --- Wait until the SDK reports the target sweep as current ---
|
|
12899
|
-
// Tries: sdk.Sweep.current.waitUntil -> sdk.Sweep.current.subscribe -> polling getCurrent/value
|
|
12900
|
-
// This function matches targetId against multiple fields and prioritizes .id matching.
|
|
12901
13180
|
waitForSweepReady(targetId, timeoutMs = 10000) {
|
|
12902
13181
|
const sdk = this.sdk;
|
|
12903
|
-
// helper to match multiple id fields, prioritizing id first
|
|
12904
13182
|
const matchesTarget = (data) => {
|
|
12905
13183
|
if (!data)
|
|
12906
13184
|
return false;
|
|
12907
|
-
// check .id first (legacy), then uuid, sid, uid
|
|
12908
13185
|
const fields = [data.id, data.uuid, data.sid, data.uid];
|
|
12909
13186
|
return fields.some(f => !!f && String(f) === String(targetId));
|
|
12910
13187
|
};
|
|
12911
|
-
// 1) prefer waitUntil if available on the observable
|
|
12912
13188
|
if (sdk.Sweep && sdk.Sweep.current && typeof sdk.Sweep.current.waitUntil === 'function') {
|
|
12913
13189
|
return Promise.race([
|
|
12914
13190
|
sdk.Sweep.current.waitUntil((data) => matchesTarget(data)),
|
|
12915
13191
|
this.timeoutPromise(timeoutMs, `Timeout waiting for sweep ${targetId}`)
|
|
12916
13192
|
]);
|
|
12917
13193
|
}
|
|
12918
|
-
// 2) subscribe if available
|
|
12919
13194
|
if (sdk.Sweep && sdk.Sweep.current && typeof sdk.Sweep.current.subscribe === 'function') {
|
|
12920
13195
|
return new Promise((resolve, reject) => {
|
|
12921
13196
|
const timer = setTimeout(() => {
|
|
@@ -12925,7 +13200,6 @@ class MatterportImportService {
|
|
|
12925
13200
|
}, timeoutMs);
|
|
12926
13201
|
let off = null;
|
|
12927
13202
|
try {
|
|
12928
|
-
// subscribe may return an unsubscribe function or an object with unsubscribe()
|
|
12929
13203
|
const subResult = sdk.Sweep.current.subscribe((data) => {
|
|
12930
13204
|
if (matchesTarget(data)) {
|
|
12931
13205
|
clearTimeout(timer);
|
|
@@ -12939,7 +13213,6 @@ class MatterportImportService {
|
|
|
12939
13213
|
resolve();
|
|
12940
13214
|
}
|
|
12941
13215
|
});
|
|
12942
|
-
// if subscribe returned an unsubscribe function, keep it as off
|
|
12943
13216
|
if (typeof subResult === 'function')
|
|
12944
13217
|
off = subResult;
|
|
12945
13218
|
else if (subResult && typeof subResult.unsubscribe === 'function')
|
|
@@ -12951,7 +13224,6 @@ class MatterportImportService {
|
|
|
12951
13224
|
}
|
|
12952
13225
|
});
|
|
12953
13226
|
}
|
|
12954
|
-
// 3) polling fallback: try getCurrent(), sdk.Sweep.current() function, or .value, else poll renderer readiness
|
|
12955
13227
|
return new Promise(async (resolve, reject) => {
|
|
12956
13228
|
const pollInterval = 200;
|
|
12957
13229
|
const timer = setTimeout(() => {
|
|
@@ -12965,11 +13237,9 @@ class MatterportImportService {
|
|
|
12965
13237
|
current = await sdk.Sweep.getCurrent();
|
|
12966
13238
|
}
|
|
12967
13239
|
else if (sdk.Sweep && typeof sdk.Sweep.current === 'function') {
|
|
12968
|
-
// some builds expose a function
|
|
12969
13240
|
current = await sdk.Sweep.current();
|
|
12970
13241
|
}
|
|
12971
13242
|
else if (sdk.Sweep && sdk.Sweep.current && sdk.Sweep.current.value) {
|
|
12972
|
-
// some observables expose a .value
|
|
12973
13243
|
current = sdk.Sweep.current.value;
|
|
12974
13244
|
}
|
|
12975
13245
|
if (matchesTarget(current)) {
|
|
@@ -12982,12 +13252,10 @@ class MatterportImportService {
|
|
|
12982
13252
|
// ignore and retry until timeout
|
|
12983
13253
|
}
|
|
12984
13254
|
};
|
|
12985
|
-
// initial immediate check
|
|
12986
13255
|
await checkNow();
|
|
12987
13256
|
const interval = setInterval(checkNow, pollInterval);
|
|
12988
13257
|
});
|
|
12989
13258
|
}
|
|
12990
|
-
// --- small helpers ---
|
|
12991
13259
|
sleep(ms) {
|
|
12992
13260
|
return new Promise(res => setTimeout(res, ms));
|
|
12993
13261
|
}
|
|
@@ -13008,7 +13276,6 @@ class MatterportImportService {
|
|
|
13008
13276
|
console.log('Floor are not loaded yet');
|
|
13009
13277
|
return;
|
|
13010
13278
|
}
|
|
13011
|
-
// look up for sequence number (the safest method)
|
|
13012
13279
|
let floorMatterport = Object.values(this.floors).find((floor) => floor.sequence === matterportFloorSequence);
|
|
13013
13280
|
if (!floorMatterport) {
|
|
13014
13281
|
floorMatterport = Object.values(this.floors).find((floor) => floorName.includes(floor.name) && floor.name != '');
|
|
@@ -13016,13 +13283,11 @@ class MatterportImportService {
|
|
|
13016
13283
|
if (!floorMatterport) {
|
|
13017
13284
|
floorMatterport = Object.values(this.floors).find((floor) => floorName.includes(floor.id));
|
|
13018
13285
|
}
|
|
13019
|
-
// console.log(floorMatterport)
|
|
13020
13286
|
if (floorMatterport) {
|
|
13021
13287
|
let retry = true;
|
|
13022
13288
|
while (retry) {
|
|
13023
13289
|
try {
|
|
13024
13290
|
const floorIndex = await this.sdk.Floor.moveTo(floorMatterport.sequence);
|
|
13025
|
-
// console.log("moved to floorIndex", floorIndex);
|
|
13026
13291
|
retry = false;
|
|
13027
13292
|
}
|
|
13028
13293
|
catch (error) {
|
|
@@ -13041,13 +13306,10 @@ class MatterportImportService {
|
|
|
13041
13306
|
const nmbFloors = Object.keys(this.floors).length;
|
|
13042
13307
|
console.log('Total number of floors', nmbFloors);
|
|
13043
13308
|
for (let index = 0; index < nmbFloors; index += 1) {
|
|
13044
|
-
// eslint-disable-next-line no-await-in-loop
|
|
13045
13309
|
console.log('Importing floor', index);
|
|
13046
13310
|
await this.sdk.Floor.moveTo(index).then(async () => {
|
|
13047
13311
|
await wait(700);
|
|
13048
|
-
// console.log("about to save the floor", index);
|
|
13049
13312
|
const zone = await this.saveFloor(defautZoneID, node);
|
|
13050
|
-
// console.log("received floor zone", zone);
|
|
13051
13313
|
zones.push(zone);
|
|
13052
13314
|
});
|
|
13053
13315
|
}
|
|
@@ -13065,14 +13327,12 @@ class MatterportImportService {
|
|
|
13065
13327
|
console.log('sweeps for floor', sweepsForFloor.length);
|
|
13066
13328
|
const sweepIDs = sweepsForFloor.map((sweep) => sweep.uuid);
|
|
13067
13329
|
const zone = await this.createZone(`Étage ${name} - ${node.metadata}`, layerFloor.id, null, defautZoneID, sweepIDs, node, JSON.stringify({ matterportFloorSequence: this.currentFloor.sequence }));
|
|
13068
|
-
// TODO link to scans
|
|
13069
13330
|
const base64 = await this.sdk.Renderer.takeScreenShot({
|
|
13070
13331
|
width: 4096,
|
|
13071
13332
|
height: 2048,
|
|
13072
13333
|
});
|
|
13073
13334
|
const spaceIDshort = this.spaceID.slice(0, 8);
|
|
13074
13335
|
await uploadBase64Image(base64, null, `visits/${this.modelID}/plans/`, `plan_floor_${this.currentFloor.id}_${spaceIDshort}`);
|
|
13075
|
-
// create Plan object
|
|
13076
13336
|
const plan = {
|
|
13077
13337
|
name: `plan_floor_${this.currentFloor.id}`,
|
|
13078
13338
|
annexe: `visits/${this.modelID}/plans/plan_floor_${this.currentFloor.id}_${spaceIDshort}.jpeg`,
|
|
@@ -13083,7 +13343,6 @@ class MatterportImportService {
|
|
|
13083
13343
|
isImportedMatterport: true,
|
|
13084
13344
|
};
|
|
13085
13345
|
await this.planService.createPlan(plan);
|
|
13086
|
-
// console.log("floor saved");
|
|
13087
13346
|
return Promise.resolve(zone);
|
|
13088
13347
|
}
|
|
13089
13348
|
abandon() {
|
|
@@ -13105,10 +13364,9 @@ class MatterportImportService {
|
|
|
13105
13364
|
if (layers.length > 0) {
|
|
13106
13365
|
return layers[0];
|
|
13107
13366
|
}
|
|
13108
|
-
// create then
|
|
13109
13367
|
return this.layerService.createLayerForOrganisation(name, currentOrgId);
|
|
13110
13368
|
}
|
|
13111
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportImportService, deps: [{ token: NavigationService }, { token: ZoneService }, { token: ViewerService }, { token: LayerService }, { token: BaseUserService }, { token: PlanService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
13369
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportImportService, deps: [{ token: MATTERPORT_SDK_KEY }, { token: NavigationService }, { token: ZoneService }, { token: ViewerService }, { token: LayerService }, { token: BaseUserService }, { token: PlanService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
13112
13370
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportImportService, providedIn: 'root' });
|
|
13113
13371
|
}
|
|
13114
13372
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportImportService, decorators: [{
|
|
@@ -13116,7 +13374,49 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
13116
13374
|
args: [{
|
|
13117
13375
|
providedIn: 'root',
|
|
13118
13376
|
}]
|
|
13119
|
-
}], ctorParameters: () => [{ type:
|
|
13377
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
13378
|
+
type: Inject,
|
|
13379
|
+
args: [MATTERPORT_SDK_KEY]
|
|
13380
|
+
}] }, { type: NavigationService }, { type: ZoneService }, { type: ViewerService }, { type: LayerService }, { type: BaseUserService }, { type: PlanService }] });
|
|
13381
|
+
|
|
13382
|
+
// matterport-transform.util.ts
|
|
13383
|
+
/**
|
|
13384
|
+
* Computes the rigid transform (Y-axis rotation + translation) between two
|
|
13385
|
+
* coordinate frames, from 2 pairs of points representing the same physical spot.
|
|
13386
|
+
*/
|
|
13387
|
+
function computeRigidTransform(sourceA, targetA, sourceB, targetB) {
|
|
13388
|
+
const vSource = { x: sourceB.x - sourceA.x, z: sourceB.z - sourceA.z };
|
|
13389
|
+
const vTarget = { x: targetB.x - targetA.x, z: targetB.z - targetA.z };
|
|
13390
|
+
const angleSource = Math.atan2(vSource.z, vSource.x);
|
|
13391
|
+
const angleTarget = Math.atan2(vTarget.z, vTarget.x);
|
|
13392
|
+
return {
|
|
13393
|
+
rotationYRad: angleTarget - angleSource,
|
|
13394
|
+
refSource: sourceA,
|
|
13395
|
+
refTarget: targetA
|
|
13396
|
+
};
|
|
13397
|
+
}
|
|
13398
|
+
/** Applies the rigid transform to a 3D position (x, y, z). */
|
|
13399
|
+
function applyRigidTransformToPoint(p, t) {
|
|
13400
|
+
const dx = p.x - t.refSource.x;
|
|
13401
|
+
const dz = p.z - t.refSource.z;
|
|
13402
|
+
const cos = Math.cos(t.rotationYRad);
|
|
13403
|
+
const sin = Math.sin(t.rotationYRad);
|
|
13404
|
+
return {
|
|
13405
|
+
x: t.refTarget.x + (dx * cos - dz * sin),
|
|
13406
|
+
y: t.refTarget.y + (p.y - t.refSource.y), // vertical axis is not rotated
|
|
13407
|
+
z: t.refTarget.z + (dx * sin + dz * cos)
|
|
13408
|
+
};
|
|
13409
|
+
}
|
|
13410
|
+
/** Applies the Y rotation (in degrees) to an orientation angle (a pose's yRotation). */
|
|
13411
|
+
function applyRigidTransformToYRotationDeg(yRotationDeg, t) {
|
|
13412
|
+
const deltaDeg = t.rotationYRad * (180 / Math.PI);
|
|
13413
|
+
return yRotationDeg + deltaDeg;
|
|
13414
|
+
}
|
|
13415
|
+
/** Euclidean distance between two points, useful to validate landmark quality. */
|
|
13416
|
+
function distance(a, b) {
|
|
13417
|
+
const dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z;
|
|
13418
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
13419
|
+
}
|
|
13120
13420
|
|
|
13121
13421
|
/* eslint-disable class-methods-use-this */
|
|
13122
13422
|
class Object3DService {
|
|
@@ -13484,6 +13784,855 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
13484
13784
|
args: ['currentAPIService']
|
|
13485
13785
|
}] }, { type: BaseUserService }, { type: NavigatorService }, { type: ZoneChangeService }, { type: FilterService }] });
|
|
13486
13786
|
|
|
13787
|
+
/** Safely extracts a readable message from an unknown catch value, including Amplify's
|
|
13788
|
+
* GraphQL error shape ({ data, errors: [{ errorType, message }] }). */
|
|
13789
|
+
function toErrorMessage(err) {
|
|
13790
|
+
if (err && typeof err === 'object') {
|
|
13791
|
+
const anyErr = err;
|
|
13792
|
+
if (Array.isArray(anyErr.errors) && anyErr.errors.length > 0) {
|
|
13793
|
+
return anyErr.errors
|
|
13794
|
+
.map((e) => {
|
|
13795
|
+
const title = e.errorType ? `${e.errorType}: ` : '';
|
|
13796
|
+
return `${title}${e.message || 'Unknown error'}`;
|
|
13797
|
+
})
|
|
13798
|
+
.join(' | ');
|
|
13799
|
+
}
|
|
13800
|
+
if (err instanceof Error) {
|
|
13801
|
+
return err.message;
|
|
13802
|
+
}
|
|
13803
|
+
}
|
|
13804
|
+
if (typeof err === 'string') {
|
|
13805
|
+
return err;
|
|
13806
|
+
}
|
|
13807
|
+
try {
|
|
13808
|
+
return JSON.stringify(err);
|
|
13809
|
+
}
|
|
13810
|
+
catch {
|
|
13811
|
+
return String(err);
|
|
13812
|
+
}
|
|
13813
|
+
}
|
|
13814
|
+
/** Thrown when the referenced S3 source file doesn't exist (orphaned DB reference —
|
|
13815
|
+
* e.g. the file was deleted or renamed on S3 without updating the database).
|
|
13816
|
+
* Kept distinct from other copy failures so the calling loop can log a clear,
|
|
13817
|
+
* specific message and skip the whole item instead of a generic error. */
|
|
13818
|
+
class MissingS3FileError extends Error {
|
|
13819
|
+
sourceKey;
|
|
13820
|
+
contextLabel;
|
|
13821
|
+
constructor(sourceKey, contextLabel) {
|
|
13822
|
+
super(`Source file missing on S3: "${sourceKey}" (${contextLabel})`);
|
|
13823
|
+
this.sourceKey = sourceKey;
|
|
13824
|
+
this.contextLabel = contextLabel;
|
|
13825
|
+
this.name = 'MissingS3FileError';
|
|
13826
|
+
}
|
|
13827
|
+
}
|
|
13828
|
+
class MatterportTagCopyService {
|
|
13829
|
+
navigationService;
|
|
13830
|
+
userService;
|
|
13831
|
+
poiService;
|
|
13832
|
+
ticketService;
|
|
13833
|
+
equipmentService;
|
|
13834
|
+
object3DService;
|
|
13835
|
+
featureService;
|
|
13836
|
+
measurementService;
|
|
13837
|
+
commentService;
|
|
13838
|
+
translate;
|
|
13839
|
+
// Emits progress updates during copyTagsFromVisit(); null when idle
|
|
13840
|
+
copyProgress$ = new BehaviorSubject(null);
|
|
13841
|
+
// Emits the accumulated log lines during copyTagsFromVisit(); [] when idle
|
|
13842
|
+
copyLog$ = new BehaviorSubject([]);
|
|
13843
|
+
spModule;
|
|
13844
|
+
log = [];
|
|
13845
|
+
errorSeq = 0;
|
|
13846
|
+
constructor(navigationService, userService, poiService, ticketService, equipmentService, object3DService, featureService, measurementService, commentService, translate) {
|
|
13847
|
+
this.navigationService = navigationService;
|
|
13848
|
+
this.userService = userService;
|
|
13849
|
+
this.poiService = poiService;
|
|
13850
|
+
this.ticketService = ticketService;
|
|
13851
|
+
this.equipmentService = equipmentService;
|
|
13852
|
+
this.object3DService = object3DService;
|
|
13853
|
+
this.featureService = featureService;
|
|
13854
|
+
this.measurementService = measurementService;
|
|
13855
|
+
this.commentService = commentService;
|
|
13856
|
+
this.translate = translate;
|
|
13857
|
+
this.spModule = userService.getSpModule();
|
|
13858
|
+
}
|
|
13859
|
+
//
|
|
13860
|
+
// ---------- Public API ----------
|
|
13861
|
+
//
|
|
13862
|
+
async copyTagsFromVisit(sourceSpaceId, targetSpaceId, landmarkSourceA, landmarkTargetA, landmarkSourceB, landmarkTargetB) {
|
|
13863
|
+
let errorCount = 0;
|
|
13864
|
+
this.resetLog();
|
|
13865
|
+
try {
|
|
13866
|
+
console.log(`Copying Tags from source ${sourceSpaceId} to target ${targetSpaceId}`);
|
|
13867
|
+
this.setStepProgress('collecting', 'collecting', 0);
|
|
13868
|
+
this.copyProgress$.next({
|
|
13869
|
+
phase: 'collecting',
|
|
13870
|
+
processedInPhase: 0, totalInPhase: 0,
|
|
13871
|
+
processedOverall: 0, totalOverall: 0,
|
|
13872
|
+
percent: 0, errorCount: 0
|
|
13873
|
+
});
|
|
13874
|
+
const targetMission = this.userService.currentMission(targetSpaceId);
|
|
13875
|
+
const targetMissionId = targetMission ? targetMission.id : null;
|
|
13876
|
+
const transform = computeRigidTransform(landmarkSourceA, landmarkTargetA, landmarkSourceB, landmarkTargetB);
|
|
13877
|
+
console.log('Rigid transform computed:', {
|
|
13878
|
+
rotationYDeg: transform.rotationYRad * (180 / Math.PI),
|
|
13879
|
+
refSource: transform.refSource,
|
|
13880
|
+
refTarget: transform.refTarget
|
|
13881
|
+
});
|
|
13882
|
+
const sourceSweepIds = await this.navigationService.getAllowedSweepsIDsForUserInSpace(sourceSpaceId);
|
|
13883
|
+
const targetSweepIds = await this.navigationService.getAllowedSweepsIDsForUserInSpace(targetSpaceId);
|
|
13884
|
+
this.setStepProgress('collecting', 'collecting', 30);
|
|
13885
|
+
const [sourceNavPositions, targetNavPositions] = await Promise.all([
|
|
13886
|
+
this.buildNavigationPositionMap(sourceSpaceId, sourceSweepIds),
|
|
13887
|
+
this.buildNavigationPositionMap(targetSpaceId, targetSweepIds)
|
|
13888
|
+
]);
|
|
13889
|
+
const elementIds = {};
|
|
13890
|
+
let sourceTickets = [];
|
|
13891
|
+
let sourceEquipments = [];
|
|
13892
|
+
let targetTickets = [];
|
|
13893
|
+
let targetEquipments = [];
|
|
13894
|
+
let sourceMeasurements = [];
|
|
13895
|
+
let targetMeasurements = [];
|
|
13896
|
+
if (this.spModule == SpModule.IMMO) {
|
|
13897
|
+
sourceTickets = await this.ticketService.getTicketsBySpace(sourceSpaceId);
|
|
13898
|
+
sourceEquipments = await this.equipmentService.getEquipmentsForSpace(sourceSpaceId);
|
|
13899
|
+
targetTickets = await this.ticketService.getTicketsBySpace(targetSpaceId);
|
|
13900
|
+
targetEquipments = await this.equipmentService.getEquipmentsForSpace(targetSpaceId);
|
|
13901
|
+
}
|
|
13902
|
+
this.setStepProgress('collecting', 'collecting', 55);
|
|
13903
|
+
const sourceObjects3D = await this.object3DService.getObjects3DBySpace(sourceSpaceId);
|
|
13904
|
+
const targetObjects3D = await this.object3DService.getObjects3DBySpace(targetSpaceId);
|
|
13905
|
+
const sourceFeatures = await this.featureService.getFeaturesBySpaceWithoutFiltering(sourceSpaceId);
|
|
13906
|
+
const targetFeatures = await this.featureService.getFeaturesBySpaceWithoutFiltering(targetSpaceId);
|
|
13907
|
+
this.setStepProgress('collecting', 'collecting', 75);
|
|
13908
|
+
if (this.spModule == SpModule.IMMO) {
|
|
13909
|
+
sourceMeasurements = await this.measurementService.getMeasurementsForSpaceForUser(sourceSpaceId);
|
|
13910
|
+
targetMeasurements = await this.measurementService.getMeasurementsForSpaceForUser(targetSpaceId);
|
|
13911
|
+
}
|
|
13912
|
+
this.setStepProgress('collecting', 'collecting', 90);
|
|
13913
|
+
const sourcePoisAll = await this.poiService.getPoisByMatterportSweeps(sourceSweepIds);
|
|
13914
|
+
const targetPoisAll = await this.poiService.getPoisByMatterportSweeps(targetSweepIds);
|
|
13915
|
+
const handledPoiTypes = new Set([
|
|
13916
|
+
PoiType.TICKET, PoiType.EQUIPMENT, PoiType.DATA, PoiType.DESK,
|
|
13917
|
+
PoiType.OBJECT3D, PoiType.MEASURE
|
|
13918
|
+
]);
|
|
13919
|
+
const sourcePois = sourcePoisAll.filter(p => !handledPoiTypes.has(p.type));
|
|
13920
|
+
const targetPois = targetPoisAll.filter(p => !handledPoiTypes.has(p.type));
|
|
13921
|
+
this.setStepProgress('collecting', 'collecting', 100);
|
|
13922
|
+
const totalOverall = sourceTickets.length + sourceEquipments.length + sourcePois.length
|
|
13923
|
+
+ sourceObjects3D.length + sourceFeatures.length + sourceMeasurements.length;
|
|
13924
|
+
let processedOverall = 0;
|
|
13925
|
+
const emitProgress = (phase, processedInPhase, totalInPhase) => {
|
|
13926
|
+
this.copyProgress$.next({
|
|
13927
|
+
phase,
|
|
13928
|
+
processedInPhase,
|
|
13929
|
+
totalInPhase,
|
|
13930
|
+
processedOverall,
|
|
13931
|
+
totalOverall,
|
|
13932
|
+
percent: totalOverall > 0 ? Math.round((processedOverall / totalOverall) * 100) : 0,
|
|
13933
|
+
errorCount
|
|
13934
|
+
});
|
|
13935
|
+
};
|
|
13936
|
+
if (this.spModule == SpModule.IMMO) {
|
|
13937
|
+
// TICKETS
|
|
13938
|
+
for (let i = 0; i < sourceTickets.length; i++) {
|
|
13939
|
+
const sourceObj = sourceTickets[i];
|
|
13940
|
+
try {
|
|
13941
|
+
const existsInTarget = this.matchesExisting(sourceObj, targetTickets);
|
|
13942
|
+
if (!existsInTarget) {
|
|
13943
|
+
const rawSource = sourceObj;
|
|
13944
|
+
const ticketInput = {
|
|
13945
|
+
title: rawSource.title,
|
|
13946
|
+
friendlyLabel: rawSource.friendlyLabel,
|
|
13947
|
+
status: rawSource.status,
|
|
13948
|
+
type: rawSource.type,
|
|
13949
|
+
priority: rawSource.priority,
|
|
13950
|
+
description: rawSource.description,
|
|
13951
|
+
level: rawSource.level,
|
|
13952
|
+
metadata: rawSource.metadata || null,
|
|
13953
|
+
read: rawSource.read || null,
|
|
13954
|
+
projectID: rawSource.projectID || null,
|
|
13955
|
+
problemCode: rawSource.problemCode || null,
|
|
13956
|
+
archivedForMissionID: rawSource.archivedForMissionID || null,
|
|
13957
|
+
spaceID: targetSpaceId,
|
|
13958
|
+
parentID: null,
|
|
13959
|
+
domainID: null,
|
|
13960
|
+
ownerMissionID: targetMissionId
|
|
13961
|
+
};
|
|
13962
|
+
const createdObj = await this.ticketService.createTicket(ticketInput);
|
|
13963
|
+
if (createdObj && createdObj.id) {
|
|
13964
|
+
elementIds[sourceObj.id] = createdObj.id;
|
|
13965
|
+
}
|
|
13966
|
+
if (sourceObj.pois.items && sourceObj.pois.items.length > 0) {
|
|
13967
|
+
const sourcePoi = sourceObj.pois.items[0];
|
|
13968
|
+
const targetSweepId = this.findClosestTargetSweepId(sourcePoi.matterportSweepID, sourceNavPositions, targetNavPositions, transform);
|
|
13969
|
+
const adjustedCoordinate = this.getAdjustedCoordinates(sourcePoi.coordinate, transform);
|
|
13970
|
+
const poiInput = {
|
|
13971
|
+
coordinate: JSON.stringify(adjustedCoordinate),
|
|
13972
|
+
type: sourcePoi.type,
|
|
13973
|
+
elementID: createdObj.id,
|
|
13974
|
+
metadata: sourcePoi.metadata,
|
|
13975
|
+
matterportSweepID: targetSweepId
|
|
13976
|
+
};
|
|
13977
|
+
await this.poiService.createPOIForSpaceID(poiInput);
|
|
13978
|
+
}
|
|
13979
|
+
}
|
|
13980
|
+
}
|
|
13981
|
+
catch (itemError) {
|
|
13982
|
+
console.error(`Error copying ticket "${sourceObj?.title}" (id: ${sourceObj?.id}):`, itemError);
|
|
13983
|
+
errorCount++;
|
|
13984
|
+
this.logCopyItemError(itemError, 'errorTicket', sourceObj?.title || sourceObj?.id);
|
|
13985
|
+
}
|
|
13986
|
+
processedOverall++;
|
|
13987
|
+
emitProgress('tickets', i + 1, sourceTickets.length);
|
|
13988
|
+
const pct = sourceTickets.length > 0 ? Math.round(((i + 1) / sourceTickets.length) * 100) : 100;
|
|
13989
|
+
this.setStepProgress('tickets', 'tickets', pct);
|
|
13990
|
+
}
|
|
13991
|
+
// EQUIPMENTS (title, POI, PHOTO and NOTES/documents all copied — files physically
|
|
13992
|
+
// duplicated in S3 first, so the copy stays independent from the source)
|
|
13993
|
+
for (let i = 0; i < sourceEquipments.length; i++) {
|
|
13994
|
+
const sourceObj = sourceEquipments[i];
|
|
13995
|
+
try {
|
|
13996
|
+
const existsInTarget = this.matchesExisting(sourceObj, targetEquipments);
|
|
13997
|
+
if (!existsInTarget) {
|
|
13998
|
+
const rawSource = sourceObj;
|
|
13999
|
+
let copiedPhotoKey = null;
|
|
14000
|
+
if (rawSource.photo) {
|
|
14001
|
+
copiedPhotoKey = await this.copyS3KeyIfPresent(rawSource.photo, `equipment photo "${rawSource.title}"`);
|
|
14002
|
+
}
|
|
14003
|
+
const equipmentInput = {
|
|
14004
|
+
title: rawSource.title,
|
|
14005
|
+
status: rawSource.status,
|
|
14006
|
+
description: rawSource.description,
|
|
14007
|
+
archivedForMissionID: rawSource.archivedForMissionID || null,
|
|
14008
|
+
spaceID: targetSpaceId,
|
|
14009
|
+
domainID: null,
|
|
14010
|
+
photo: copiedPhotoKey,
|
|
14011
|
+
};
|
|
14012
|
+
const createdObj = await this.equipmentService.createEquipment(equipmentInput, null, null);
|
|
14013
|
+
if (createdObj && createdObj.id) {
|
|
14014
|
+
elementIds[sourceObj.id] = createdObj.id;
|
|
14015
|
+
}
|
|
14016
|
+
// Copy notes (documents, technical/financial/other/contractual/image notes)
|
|
14017
|
+
if (createdObj && rawSource.notes?.items && rawSource.notes.items.length > 0) {
|
|
14018
|
+
for (const sourceNote of rawSource.notes.items) {
|
|
14019
|
+
try {
|
|
14020
|
+
if (!sourceNote.attachment) {
|
|
14021
|
+
continue; // nothing to duplicate for this note
|
|
14022
|
+
}
|
|
14023
|
+
const copiedKey = await this.copyS3KeyIfPresent(sourceNote.attachment, `equipment note for "${sourceObj?.title}"`);
|
|
14024
|
+
await this.equipmentService.createNoteWithExistingAttachment(createdObj.id, copiedKey, sourceNote.type, targetMissionId);
|
|
14025
|
+
}
|
|
14026
|
+
catch (noteError) {
|
|
14027
|
+
console.error(`Error copying note for equipment "${sourceObj?.title}":`, noteError);
|
|
14028
|
+
errorCount++;
|
|
14029
|
+
this.logCopyItemError(noteError, 'errorEquipmentNote', sourceObj?.title || sourceObj?.id);
|
|
14030
|
+
}
|
|
14031
|
+
}
|
|
14032
|
+
}
|
|
14033
|
+
if (sourceObj.pois.items && sourceObj.pois.items.length > 0) {
|
|
14034
|
+
const sourcePoi = sourceObj.pois.items[0];
|
|
14035
|
+
const targetSweepId = this.findClosestTargetSweepId(sourcePoi.matterportSweepID, sourceNavPositions, targetNavPositions, transform);
|
|
14036
|
+
const adjustedCoordinate = this.getAdjustedCoordinates(sourcePoi.coordinate, transform);
|
|
14037
|
+
const poiInput = {
|
|
14038
|
+
coordinate: JSON.stringify(adjustedCoordinate),
|
|
14039
|
+
type: sourcePoi.type,
|
|
14040
|
+
elementID: createdObj.id,
|
|
14041
|
+
metadata: sourcePoi.metadata,
|
|
14042
|
+
matterportSweepID: targetSweepId
|
|
14043
|
+
};
|
|
14044
|
+
await this.poiService.createPOIForSpaceID(poiInput);
|
|
14045
|
+
}
|
|
14046
|
+
}
|
|
14047
|
+
}
|
|
14048
|
+
catch (itemError) {
|
|
14049
|
+
console.error(`Error copying equipment "${sourceObj?.title}" (id: ${sourceObj?.id}):`, itemError);
|
|
14050
|
+
errorCount++;
|
|
14051
|
+
this.logCopyItemError(itemError, 'errorEquipment', sourceObj?.title || sourceObj?.id);
|
|
14052
|
+
}
|
|
14053
|
+
processedOverall++;
|
|
14054
|
+
emitProgress('equipments', i + 1, sourceEquipments.length);
|
|
14055
|
+
const pct = sourceEquipments.length > 0 ? Math.round(((i + 1) / sourceEquipments.length) * 100) : 100;
|
|
14056
|
+
this.setStepProgress('equipments', 'equipments', pct);
|
|
14057
|
+
}
|
|
14058
|
+
// Second pass: remap subequipmentsIDs on newly created equipments to point to
|
|
14059
|
+
// their copied counterparts. IDs with no copied counterpart (source equipment
|
|
14060
|
+
// wasn't copied — e.g. it already existed in target, or was skipped) are dropped
|
|
14061
|
+
// rather than left dangling or pointing back at the source space's equipment.
|
|
14062
|
+
for (const sourceObj of sourceEquipments) {
|
|
14063
|
+
const rawSource = sourceObj;
|
|
14064
|
+
const newId = elementIds[sourceObj.id];
|
|
14065
|
+
if (newId && rawSource.subequipmentsIDs && rawSource.subequipmentsIDs.length > 0) {
|
|
14066
|
+
const remapped = rawSource.subequipmentsIDs
|
|
14067
|
+
.map((subId) => elementIds[subId])
|
|
14068
|
+
.filter((id) => !!id);
|
|
14069
|
+
if (remapped.length > 0) {
|
|
14070
|
+
try {
|
|
14071
|
+
await this.equipmentService.updateEquipment({ id: newId, subequipmentsIDs: remapped });
|
|
14072
|
+
}
|
|
14073
|
+
catch (subEquipError) {
|
|
14074
|
+
console.error(`Error remapping sub-equipments for "${sourceObj?.title}":`, subEquipError);
|
|
14075
|
+
errorCount++;
|
|
14076
|
+
this.logCopyItemError(subEquipError, 'errorEquipmentSubEquip', sourceObj?.title || sourceObj?.id);
|
|
14077
|
+
}
|
|
14078
|
+
}
|
|
14079
|
+
}
|
|
14080
|
+
}
|
|
14081
|
+
}
|
|
14082
|
+
// POIS (standalone — types already handled above are excluded)
|
|
14083
|
+
for (let i = 0; i < sourcePois.length; i++) {
|
|
14084
|
+
const sourcePoi = sourcePois[i];
|
|
14085
|
+
try {
|
|
14086
|
+
const existsInTarget = targetPois.some(tp => tp.coordinate === sourcePoi.coordinate && tp.tagDescription === sourcePoi.tagDescription);
|
|
14087
|
+
if (!existsInTarget) {
|
|
14088
|
+
const targetSweepId = this.findClosestTargetSweepId(sourcePoi.matterportSweepID, sourceNavPositions, targetNavPositions, transform);
|
|
14089
|
+
const adjustedCoordinate = this.getAdjustedCoordinates(sourcePoi.coordinate, transform);
|
|
14090
|
+
let elementID = sourcePoi.elementID;
|
|
14091
|
+
if (elementID && elementIds[elementID]) {
|
|
14092
|
+
elementID = elementIds[elementID];
|
|
14093
|
+
}
|
|
14094
|
+
const copiedTagIcon = await this.duplicateTagIcon(sourcePoi.tagIcon);
|
|
14095
|
+
const copiedTagImageID = await this.duplicateTagImageID(sourcePoi.tagImageID);
|
|
14096
|
+
const newPoi = {
|
|
14097
|
+
coordinate: JSON.stringify(adjustedCoordinate),
|
|
14098
|
+
type: sourcePoi.type,
|
|
14099
|
+
elementID: elementID,
|
|
14100
|
+
tagDescription: sourcePoi.tagDescription,
|
|
14101
|
+
tagIcon: copiedTagIcon,
|
|
14102
|
+
tagImageID: copiedTagImageID,
|
|
14103
|
+
virtual: sourcePoi.virtual,
|
|
14104
|
+
xRotation: sourcePoi.xRotation,
|
|
14105
|
+
yRotation: sourcePoi.yRotation,
|
|
14106
|
+
metadata: sourcePoi.metadata,
|
|
14107
|
+
matterportSweepID: targetSweepId
|
|
14108
|
+
};
|
|
14109
|
+
await this.poiService.createPOIForSpaceID(newPoi);
|
|
14110
|
+
}
|
|
14111
|
+
}
|
|
14112
|
+
catch (itemError) {
|
|
14113
|
+
console.error(`Error copying POI (sweep: ${sourcePoi?.matterportSweepID}):`, itemError);
|
|
14114
|
+
errorCount++;
|
|
14115
|
+
this.logCopyItemError(itemError, 'errorPoi', sourcePoi?.matterportSweepID);
|
|
14116
|
+
}
|
|
14117
|
+
processedOverall++;
|
|
14118
|
+
emitProgress('pois', i + 1, sourcePois.length);
|
|
14119
|
+
const pct = sourcePois.length > 0 ? Math.round(((i + 1) / sourcePois.length) * 100) : 100;
|
|
14120
|
+
this.setStepProgress('pois', 'pois', pct);
|
|
14121
|
+
}
|
|
14122
|
+
// OBJECTS 3D (the 3D model file itself — `path` — is physically duplicated in S3)
|
|
14123
|
+
for (let i = 0; i < sourceObjects3D.length; i++) {
|
|
14124
|
+
const sourceObj = sourceObjects3D[i];
|
|
14125
|
+
try {
|
|
14126
|
+
const existsInTarget = targetObjects3D.some(targetObj => targetObj.name === sourceObj.name);
|
|
14127
|
+
if (!existsInTarget) {
|
|
14128
|
+
const rawSource = sourceObj;
|
|
14129
|
+
let copiedPath = rawSource.path;
|
|
14130
|
+
if (rawSource.path) {
|
|
14131
|
+
copiedPath = await this.copyS3KeyIfPresent(rawSource.path, `3D object "${rawSource.name}"`);
|
|
14132
|
+
}
|
|
14133
|
+
/**
|
|
14134
|
+
* Strict parser to ensure [float, float, float] format for GraphQL.
|
|
14135
|
+
* Forces numbers, handles potential DynamoDB object structures,
|
|
14136
|
+
* and ensures we never send [0,0,0] for scale.
|
|
14137
|
+
*/
|
|
14138
|
+
const toFloatArray = (val, defaultVal = [0, 0, 0]) => {
|
|
14139
|
+
let array = [];
|
|
14140
|
+
if (Array.isArray(val)) {
|
|
14141
|
+
// If DB format [ {N: "1"}, ... ]
|
|
14142
|
+
if (val.length > 0 && typeof val[0] === 'object' && 'N' in val[0]) {
|
|
14143
|
+
array = val.map(item => parseFloat(item.N));
|
|
14144
|
+
}
|
|
14145
|
+
// If already standard array [1, 1, 1]
|
|
14146
|
+
else {
|
|
14147
|
+
array = val.map(item => parseFloat(item));
|
|
14148
|
+
}
|
|
14149
|
+
}
|
|
14150
|
+
// If object {x, y, z}
|
|
14151
|
+
else if (val && typeof val === 'object') {
|
|
14152
|
+
array = [parseFloat(val.x), parseFloat(val.y), parseFloat(val.z)];
|
|
14153
|
+
}
|
|
14154
|
+
else {
|
|
14155
|
+
return defaultVal;
|
|
14156
|
+
}
|
|
14157
|
+
// Ensure we return valid numbers
|
|
14158
|
+
return array.map(n => isNaN(n) ? 0 : n);
|
|
14159
|
+
};
|
|
14160
|
+
const object3DInput = {
|
|
14161
|
+
path: copiedPath,
|
|
14162
|
+
name: rawSource.name,
|
|
14163
|
+
description: rawSource.description || "",
|
|
14164
|
+
viewFrustum: !!rawSource.viewFrustum,
|
|
14165
|
+
spaceID: targetSpaceId,
|
|
14166
|
+
position: toFloatArray(rawSource.position, [0, 0, 0]),
|
|
14167
|
+
rotation: toFloatArray(rawSource.rotation, [0, 0, 0]),
|
|
14168
|
+
// Enforce scale to be at least 1.0 if it's invalid
|
|
14169
|
+
scale: toFloatArray(rawSource.scale, [1, 1, 1])
|
|
14170
|
+
};
|
|
14171
|
+
// Debugging: Inspect the exact object sent to the mutation
|
|
14172
|
+
console.log(`[DEBUG] Sending Object3D "${rawSource.name}" to API:`, JSON.stringify(object3DInput));
|
|
14173
|
+
const createdObj = await this.object3DService.create(object3DInput);
|
|
14174
|
+
if (createdObj && createdObj.id) {
|
|
14175
|
+
elementIds[sourceObj.id] = createdObj.id;
|
|
14176
|
+
}
|
|
14177
|
+
if (sourceObj.pois?.items && sourceObj.pois.items.length > 0) {
|
|
14178
|
+
const sourcePoi = sourceObj.pois.items[0];
|
|
14179
|
+
const targetSweepId = this.findClosestTargetSweepId(sourcePoi.matterportSweepID, sourceNavPositions, targetNavPositions, transform);
|
|
14180
|
+
const adjustedCoordinate = this.getAdjustedCoordinates(sourcePoi.coordinate, transform);
|
|
14181
|
+
const poiInput = {
|
|
14182
|
+
coordinate: JSON.stringify(adjustedCoordinate),
|
|
14183
|
+
type: sourcePoi.type,
|
|
14184
|
+
elementID: createdObj.id,
|
|
14185
|
+
metadata: sourcePoi.metadata,
|
|
14186
|
+
matterportSweepID: targetSweepId
|
|
14187
|
+
};
|
|
14188
|
+
await this.poiService.createPOIForSpaceID(poiInput);
|
|
14189
|
+
}
|
|
14190
|
+
}
|
|
14191
|
+
}
|
|
14192
|
+
catch (itemError) {
|
|
14193
|
+
console.error(`Error copying object3D "${sourceObj?.name}":`, itemError);
|
|
14194
|
+
errorCount++;
|
|
14195
|
+
this.logCopyItemError(itemError, 'errorObject3d', sourceObj?.name || sourceObj?.id);
|
|
14196
|
+
}
|
|
14197
|
+
processedOverall++;
|
|
14198
|
+
emitProgress('objects3d', i + 1, sourceObjects3D.length);
|
|
14199
|
+
const pct = sourceObjects3D.length > 0 ? Math.round(((i + 1) / sourceObjects3D.length) * 100) : 100;
|
|
14200
|
+
this.setStepProgress('objects3d', 'objects3d', pct);
|
|
14201
|
+
}
|
|
14202
|
+
// FEATURES (title, POI, and attached comments — images/videos/docs/links — all copied,
|
|
14203
|
+
// each file physically duplicated in S3 first)
|
|
14204
|
+
for (let i = 0; i < sourceFeatures.length; i++) {
|
|
14205
|
+
const sourceObj = sourceFeatures[i];
|
|
14206
|
+
try {
|
|
14207
|
+
const existsInTarget = this.matchesExisting(sourceObj, targetFeatures);
|
|
14208
|
+
if (!existsInTarget) {
|
|
14209
|
+
const rawSource = sourceObj;
|
|
14210
|
+
const featureInput = {
|
|
14211
|
+
title: rawSource.title,
|
|
14212
|
+
shortDescription: rawSource.shortDescription,
|
|
14213
|
+
level: rawSource.level,
|
|
14214
|
+
type: rawSource.type,
|
|
14215
|
+
archivedForMissionID: rawSource.archivedForMissionID || null,
|
|
14216
|
+
stripeProductID: rawSource.stripeProductID || null,
|
|
14217
|
+
stripePriceID: rawSource.stripePriceID || null,
|
|
14218
|
+
bookingPriceMetadata: rawSource.bookingPriceMetadata || null,
|
|
14219
|
+
locale: rawSource.locale || null,
|
|
14220
|
+
hashtags: rawSource.hashtags || null,
|
|
14221
|
+
description: rawSource.description || null,
|
|
14222
|
+
domainID: null,
|
|
14223
|
+
spaceID: targetSpaceId,
|
|
14224
|
+
ownerMissionID: targetMissionId
|
|
14225
|
+
};
|
|
14226
|
+
const createdObj = await this.featureService.createFeature(featureInput);
|
|
14227
|
+
if (createdObj && createdObj.id) {
|
|
14228
|
+
elementIds[sourceObj.id] = createdObj.id;
|
|
14229
|
+
}
|
|
14230
|
+
if (createdObj && sourceObj.comments?.items && sourceObj.comments.items.length > 0) {
|
|
14231
|
+
for (const sourceComment of sourceObj.comments.items) {
|
|
14232
|
+
try {
|
|
14233
|
+
const rawComment = sourceComment;
|
|
14234
|
+
let copiedAnnexe = null;
|
|
14235
|
+
if (rawComment.annexe) {
|
|
14236
|
+
copiedAnnexe = await this.copyS3KeyIfPresent(rawComment.annexe, `feature attachment "${rawComment.title}"`);
|
|
14237
|
+
}
|
|
14238
|
+
const commentInput = {
|
|
14239
|
+
title: rawComment.title,
|
|
14240
|
+
type: rawComment.type,
|
|
14241
|
+
annexe: copiedAnnexe,
|
|
14242
|
+
externalLink: rawComment.externalLink || null,
|
|
14243
|
+
dimensions: rawComment.dimensions || null,
|
|
14244
|
+
index: rawComment.index,
|
|
14245
|
+
shownInTag: rawComment.shownInTag,
|
|
14246
|
+
isCoverImage: rawComment.isCoverImage,
|
|
14247
|
+
featureID: createdObj.id,
|
|
14248
|
+
spaceID: targetSpaceId,
|
|
14249
|
+
ownerID: targetMissionId
|
|
14250
|
+
};
|
|
14251
|
+
await this.commentService.createComment(commentInput);
|
|
14252
|
+
}
|
|
14253
|
+
catch (attachmentError) {
|
|
14254
|
+
console.error(`Error copying attachment "${sourceComment?.title}" for feature "${sourceObj?.title}":`, attachmentError);
|
|
14255
|
+
errorCount++;
|
|
14256
|
+
this.logCopyItemError(attachmentError, 'errorFeatureAttachment', sourceComment?.title || sourceComment?.id);
|
|
14257
|
+
}
|
|
14258
|
+
}
|
|
14259
|
+
}
|
|
14260
|
+
if (sourceObj.pois?.items && sourceObj.pois.items.length > 0) {
|
|
14261
|
+
const sourcePoi = sourceObj.pois.items[0];
|
|
14262
|
+
const targetSweepId = this.findClosestTargetSweepId(sourcePoi.matterportSweepID, sourceNavPositions, targetNavPositions, transform);
|
|
14263
|
+
const adjustedCoordinate = this.getAdjustedCoordinates(sourcePoi.coordinate, transform);
|
|
14264
|
+
const poiInput = {
|
|
14265
|
+
coordinate: JSON.stringify(adjustedCoordinate),
|
|
14266
|
+
type: sourcePoi.type,
|
|
14267
|
+
elementID: createdObj.id,
|
|
14268
|
+
metadata: sourcePoi.metadata,
|
|
14269
|
+
matterportSweepID: targetSweepId
|
|
14270
|
+
};
|
|
14271
|
+
await this.poiService.createPOIForSpaceID(poiInput);
|
|
14272
|
+
}
|
|
14273
|
+
}
|
|
14274
|
+
}
|
|
14275
|
+
catch (itemError) {
|
|
14276
|
+
console.error(`Error copying feature "${sourceObj?.title}" (id: ${sourceObj?.id}):`, itemError);
|
|
14277
|
+
errorCount++;
|
|
14278
|
+
this.logCopyItemError(itemError, 'errorFeature', sourceObj?.title || sourceObj?.id);
|
|
14279
|
+
}
|
|
14280
|
+
processedOverall++;
|
|
14281
|
+
emitProgress('features', i + 1, sourceFeatures.length);
|
|
14282
|
+
const pct = sourceFeatures.length > 0 ? Math.round(((i + 1) / sourceFeatures.length) * 100) : 100;
|
|
14283
|
+
this.setStepProgress('features', 'features', pct);
|
|
14284
|
+
}
|
|
14285
|
+
// MEASUREMENTS (IMMO only — a Comment + a positional-only POI, no rotation;
|
|
14286
|
+
// the screenshot file is physically duplicated in S3 first)
|
|
14287
|
+
if (this.spModule == SpModule.IMMO) {
|
|
14288
|
+
for (let i = 0; i < sourceMeasurements.length; i++) {
|
|
14289
|
+
const sourceMeasurement = sourceMeasurements[i];
|
|
14290
|
+
try {
|
|
14291
|
+
const existsInTarget = targetMeasurements.some(tm => tm.comment.title === sourceMeasurement.comment.title);
|
|
14292
|
+
if (!existsInTarget) {
|
|
14293
|
+
const rawComment = sourceMeasurement.comment;
|
|
14294
|
+
let copiedAnnexe = null;
|
|
14295
|
+
if (rawComment.annexe) {
|
|
14296
|
+
copiedAnnexe = await this.copyS3KeyIfPresent(rawComment.annexe, `measurement screenshot "${rawComment.title}"`);
|
|
14297
|
+
}
|
|
14298
|
+
const commentInput = {
|
|
14299
|
+
title: rawComment.title,
|
|
14300
|
+
type: CommentType.MEASURE,
|
|
14301
|
+
dimensions: rawComment.dimensions || null,
|
|
14302
|
+
annexe: copiedAnnexe,
|
|
14303
|
+
spaceID: targetSpaceId,
|
|
14304
|
+
};
|
|
14305
|
+
const createdComment = await this.commentService.createComment(commentInput);
|
|
14306
|
+
if (createdComment && sourceMeasurement.poi) {
|
|
14307
|
+
const targetSweepId = this.findClosestTargetSweepId(sourceMeasurement.poi.matterportSweepID, sourceNavPositions, targetNavPositions, transform);
|
|
14308
|
+
const adjustedPosition = this.getAdjustedPositionOnly(sourceMeasurement.poi.coordinate, transform);
|
|
14309
|
+
const poiInput = {
|
|
14310
|
+
coordinate: JSON.stringify(adjustedPosition),
|
|
14311
|
+
type: PoiType.MEASURE,
|
|
14312
|
+
elementID: createdComment.id,
|
|
14313
|
+
metadata: sourceMeasurement.poi.metadata,
|
|
14314
|
+
matterportSweepID: targetSweepId
|
|
14315
|
+
};
|
|
14316
|
+
await this.poiService.createPOIForSpaceID(poiInput);
|
|
14317
|
+
}
|
|
14318
|
+
}
|
|
14319
|
+
}
|
|
14320
|
+
catch (itemError) {
|
|
14321
|
+
console.error(`Error copying measurement "${sourceMeasurement?.comment?.title}":`, itemError);
|
|
14322
|
+
errorCount++;
|
|
14323
|
+
this.logCopyItemError(itemError, 'errorMeasurement', sourceMeasurement?.comment?.title || sourceMeasurement?.id);
|
|
14324
|
+
}
|
|
14325
|
+
processedOverall++;
|
|
14326
|
+
emitProgress('measurements', i + 1, sourceMeasurements.length);
|
|
14327
|
+
const pct = sourceMeasurements.length > 0 ? Math.round(((i + 1) / sourceMeasurements.length) * 100) : 100;
|
|
14328
|
+
this.setStepProgress('measurements', 'measurements', pct);
|
|
14329
|
+
}
|
|
14330
|
+
}
|
|
14331
|
+
this.copyProgress$.next({
|
|
14332
|
+
phase: 'done',
|
|
14333
|
+
processedInPhase: 0, totalInPhase: 0,
|
|
14334
|
+
processedOverall, totalOverall,
|
|
14335
|
+
percent: 100, errorCount
|
|
14336
|
+
});
|
|
14337
|
+
this.setStepLog('done', errorCount > 0
|
|
14338
|
+
? this.translate.instant('matterportCopy.doneWithErrors', { count: errorCount })
|
|
14339
|
+
: this.translate.instant('matterportCopy.done'));
|
|
14340
|
+
return true;
|
|
14341
|
+
}
|
|
14342
|
+
catch (error) {
|
|
14343
|
+
console.error('Error copying POIs:', error);
|
|
14344
|
+
this.appendErrorLog(this.translate.instant('matterportCopy.errorGeneral', { error: toErrorMessage(error) }));
|
|
14345
|
+
this.copyProgress$.next(null);
|
|
14346
|
+
return false;
|
|
14347
|
+
}
|
|
14348
|
+
}
|
|
14349
|
+
//
|
|
14350
|
+
// ---------- Progress / log helpers ----------
|
|
14351
|
+
//
|
|
14352
|
+
/** Adds or updates a step line (identified by stepId) with new text, then republishes the log. */
|
|
14353
|
+
setStepLog(stepId, text) {
|
|
14354
|
+
const existingIndex = this.log.findIndex(e => e.type === 'step' && e.id === stepId);
|
|
14355
|
+
const entry = { id: stepId, type: 'step', text };
|
|
14356
|
+
if (existingIndex >= 0) {
|
|
14357
|
+
this.log[existingIndex] = entry;
|
|
14358
|
+
}
|
|
14359
|
+
else {
|
|
14360
|
+
this.log.push(entry);
|
|
14361
|
+
}
|
|
14362
|
+
this.copyLog$.next([...this.log]);
|
|
14363
|
+
}
|
|
14364
|
+
/** Appends a new, permanent log line (never overwritten), then republishes the log. */
|
|
14365
|
+
appendErrorLog(text) {
|
|
14366
|
+
this.errorSeq++;
|
|
14367
|
+
this.log.push({ id: `error-${this.errorSeq}`, type: 'error', text });
|
|
14368
|
+
this.copyLog$.next([...this.log]);
|
|
14369
|
+
}
|
|
14370
|
+
/** Resets the log for a new run. */
|
|
14371
|
+
resetLog() {
|
|
14372
|
+
this.log = [];
|
|
14373
|
+
this.errorSeq = 0;
|
|
14374
|
+
this.copyLog$.next([]);
|
|
14375
|
+
}
|
|
14376
|
+
/** Builds and sets a step's log line as "<translated label>: <percent>%", no step numbering. */
|
|
14377
|
+
setStepProgress(stepId, translationKey, percent) {
|
|
14378
|
+
const label = this.translate.instant(`matterportCopy.${translationKey}`);
|
|
14379
|
+
this.setStepLog(stepId, `${label}: ${percent}%`);
|
|
14380
|
+
}
|
|
14381
|
+
/** Logs a translated, parameterized error line for a single failed item. */
|
|
14382
|
+
logItemError(translationKey, params) {
|
|
14383
|
+
const text = this.translate.instant(`matterportCopy.${translationKey}`, params);
|
|
14384
|
+
this.appendErrorLog(text);
|
|
14385
|
+
}
|
|
14386
|
+
/** Logs an item-level error, using a specific "missing file / orphaned reference"
|
|
14387
|
+
* translation key when the cause is a MissingS3FileError, and the generic per-type
|
|
14388
|
+
* key otherwise. Keeps the copy log readable: a missing file is not a bug to
|
|
14389
|
+
* investigate the same way a network/permission error is. */
|
|
14390
|
+
logCopyItemError(error, genericKey, itemLabel) {
|
|
14391
|
+
if (error instanceof MissingS3FileError) {
|
|
14392
|
+
this.logItemError('errorMissingS3FileSkipped', {
|
|
14393
|
+
name: itemLabel,
|
|
14394
|
+
file: error.sourceKey
|
|
14395
|
+
});
|
|
14396
|
+
}
|
|
14397
|
+
else {
|
|
14398
|
+
this.logItemError(genericKey, { name: itemLabel, error: toErrorMessage(error) });
|
|
14399
|
+
}
|
|
14400
|
+
}
|
|
14401
|
+
//
|
|
14402
|
+
// ---------- Duplicate detection ----------
|
|
14403
|
+
//
|
|
14404
|
+
/**
|
|
14405
|
+
* Heuristic duplicate-detection: compares title AND description together instead of
|
|
14406
|
+
* title alone, to reduce false positives when several source items happen to share
|
|
14407
|
+
* the same title. Not a perfect guarantee, but meaningfully more robust.
|
|
14408
|
+
*/
|
|
14409
|
+
matchesExisting(source, targetList) {
|
|
14410
|
+
return targetList.some(t => t.title === source.title && (t.description || null) === (source.description || null));
|
|
14411
|
+
}
|
|
14412
|
+
//
|
|
14413
|
+
// ---------- Sweep positions / rigid transform ----------
|
|
14414
|
+
//
|
|
14415
|
+
/**
|
|
14416
|
+
* Fetches the position of every sweep in a space and returns it as a Map for O(1) lookup.
|
|
14417
|
+
* Uses allSettled so a single failing sweep lookup doesn't abort the whole map —
|
|
14418
|
+
* that sweep is simply absent from the resulting map (and skipped later).
|
|
14419
|
+
*/
|
|
14420
|
+
async buildNavigationPositionMap(spaceId, sweepIds) {
|
|
14421
|
+
const map = new Map();
|
|
14422
|
+
const results = await Promise.allSettled(sweepIds.map(async (sid) => {
|
|
14423
|
+
const navs = await this.navigationService.getNavigationsForMatterportIDForSpace(sid, spaceId);
|
|
14424
|
+
if (navs && navs.length > 0 && navs[0].position) {
|
|
14425
|
+
return { sid, position: JSON.parse(navs[0].position) };
|
|
14426
|
+
}
|
|
14427
|
+
return null;
|
|
14428
|
+
}));
|
|
14429
|
+
for (const result of results) {
|
|
14430
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
14431
|
+
map.set(result.value.sid, result.value.position);
|
|
14432
|
+
}
|
|
14433
|
+
else if (result.status === 'rejected') {
|
|
14434
|
+
console.warn('Failed to fetch a sweep position, skipping it:', result.reason);
|
|
14435
|
+
}
|
|
14436
|
+
}
|
|
14437
|
+
return map;
|
|
14438
|
+
}
|
|
14439
|
+
/** Applies the rigid transform to a POI coordinate (position + orientation). */
|
|
14440
|
+
getAdjustedCoordinates(coordinate, transform) {
|
|
14441
|
+
if (coordinate === "null") {
|
|
14442
|
+
return null;
|
|
14443
|
+
}
|
|
14444
|
+
if (!coordinate || typeof coordinate !== 'string') {
|
|
14445
|
+
return coordinate;
|
|
14446
|
+
}
|
|
14447
|
+
const parsed = JSON.parse(coordinate);
|
|
14448
|
+
const rotatedPos = applyRigidTransformToPoint({ x: parsed.x, y: parsed.y, z: parsed.z }, transform);
|
|
14449
|
+
return {
|
|
14450
|
+
x: rotatedPos.x,
|
|
14451
|
+
y: rotatedPos.y,
|
|
14452
|
+
z: rotatedPos.z,
|
|
14453
|
+
xRotation: parsed.xRotation,
|
|
14454
|
+
yRotation: applyRigidTransformToYRotationDeg(parsed.yRotation, transform)
|
|
14455
|
+
};
|
|
14456
|
+
}
|
|
14457
|
+
/**
|
|
14458
|
+
* Applies the rigid transform's translation+rotation to a plain {x,y,z} position,
|
|
14459
|
+
* WITHOUT adding xRotation/yRotation keys. Used for measurements, whose stored
|
|
14460
|
+
* coordinate is a bare position (no orientation), unlike other POI types.
|
|
14461
|
+
*/
|
|
14462
|
+
getAdjustedPositionOnly(coordinate, transform) {
|
|
14463
|
+
if (!coordinate || typeof coordinate !== 'string' || coordinate === 'null') {
|
|
14464
|
+
return coordinate;
|
|
14465
|
+
}
|
|
14466
|
+
const parsed = JSON.parse(coordinate);
|
|
14467
|
+
return applyRigidTransformToPoint({ x: parsed.x, y: parsed.y, z: parsed.z }, transform);
|
|
14468
|
+
}
|
|
14469
|
+
/**
|
|
14470
|
+
* Finds the closest target sweep, comparing positions in the SAME coordinate frame.
|
|
14471
|
+
* Pure in-memory lookup — no network call, thanks to the pre-built position maps.
|
|
14472
|
+
*/
|
|
14473
|
+
findClosestTargetSweepId(sourceSweepID, sourceNavPositions, targetNavPositions, transform) {
|
|
14474
|
+
if (!sourceSweepID)
|
|
14475
|
+
return null;
|
|
14476
|
+
const sourcePos = sourceNavPositions.get(sourceSweepID);
|
|
14477
|
+
if (!sourcePos)
|
|
14478
|
+
return null;
|
|
14479
|
+
const sourcePosInTargetFrame = applyRigidTransformToPoint(sourcePos, transform);
|
|
14480
|
+
let closestSweepID = null;
|
|
14481
|
+
let minDistance = Infinity;
|
|
14482
|
+
for (const [targetSweepId, targetPos] of targetNavPositions.entries()) {
|
|
14483
|
+
const dx = targetPos.x - sourcePosInTargetFrame.x;
|
|
14484
|
+
const dy = targetPos.y - sourcePosInTargetFrame.y;
|
|
14485
|
+
const dz = targetPos.z - sourcePosInTargetFrame.z;
|
|
14486
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
14487
|
+
if (dist < minDistance) {
|
|
14488
|
+
minDistance = dist;
|
|
14489
|
+
closestSweepID = targetSweepId;
|
|
14490
|
+
}
|
|
14491
|
+
}
|
|
14492
|
+
return closestSweepID;
|
|
14493
|
+
}
|
|
14494
|
+
//
|
|
14495
|
+
// ---------- S3 file duplication ----------
|
|
14496
|
+
//
|
|
14497
|
+
/** Generates a short random id, used to give each copied S3 file a fresh, collision-safe key. */
|
|
14498
|
+
generateShortId() {
|
|
14499
|
+
return Math.random().toString(36).slice(2, 10);
|
|
14500
|
+
}
|
|
14501
|
+
/**
|
|
14502
|
+
* Builds a new S3 key for a copied attachment and sanitizes the filename
|
|
14503
|
+
* to prevent AWS Amplify SignatureDoesNotMatch errors.
|
|
14504
|
+
*/
|
|
14505
|
+
buildCopiedAttachmentKey(sourceAnnexe) {
|
|
14506
|
+
const lastSlash = sourceAnnexe.lastIndexOf('/');
|
|
14507
|
+
const folder = sourceAnnexe.substring(0, lastSlash + 1);
|
|
14508
|
+
let filename = sourceAnnexe.substring(lastSlash + 1);
|
|
14509
|
+
// Strip a leading "<8 hex chars>_" id prefix if present
|
|
14510
|
+
const prefixMatch = filename.match(/^[0-9a-fA-F-]{8}_(.+)$/);
|
|
14511
|
+
if (prefixMatch) {
|
|
14512
|
+
filename = prefixMatch[1];
|
|
14513
|
+
}
|
|
14514
|
+
// SANITIZATION: Replace spaces, parentheses, and accents with underscores
|
|
14515
|
+
// Crucial to prevent Amplify's Storage.copy from failing the signature calculation
|
|
14516
|
+
filename = filename.replace(/[^a-zA-Z0-9.\-_]/g, '_');
|
|
14517
|
+
const newPrefix = `${this.generateShortId()}_`;
|
|
14518
|
+
return `${folder}${newPrefix}${filename}`;
|
|
14519
|
+
}
|
|
14520
|
+
/**
|
|
14521
|
+
* Duplicates an S3 object if a key is present.
|
|
14522
|
+
* - If the source file is genuinely missing from S3 (NoSuchKey / orphaned DB reference),
|
|
14523
|
+
* throws a MissingS3FileError so the caller can log a clear, specific message and
|
|
14524
|
+
* skip the whole item entirely, rather than creating a record with a broken file link.
|
|
14525
|
+
* - Any other kind of failure (permissions, network, etc.) still throws a generic Error.
|
|
14526
|
+
*/
|
|
14527
|
+
async copyS3KeyIfPresent(key, contextLabel) {
|
|
14528
|
+
if (!key) {
|
|
14529
|
+
return null;
|
|
14530
|
+
}
|
|
14531
|
+
const sourceKey = this.extractS3KeyFromValue(key);
|
|
14532
|
+
const destKey = this.buildCopiedAttachmentKey(sourceKey);
|
|
14533
|
+
// Log the source key to ensure it matches the real S3 object path
|
|
14534
|
+
console.log(`[DEBUG] Attempting S3 copy from source: "${sourceKey}" to dest: "${destKey}"`);
|
|
14535
|
+
try {
|
|
14536
|
+
const copied = await copyFileInS3(sourceKey, destKey);
|
|
14537
|
+
if (!copied) {
|
|
14538
|
+
throw new MissingS3FileError(sourceKey, contextLabel);
|
|
14539
|
+
}
|
|
14540
|
+
return copied;
|
|
14541
|
+
}
|
|
14542
|
+
catch (error) {
|
|
14543
|
+
const isMissingFile = error instanceof MissingS3FileError
|
|
14544
|
+
|| error?.code === 'NoSuchKey'
|
|
14545
|
+
|| /NoSuchKey|does not exist/i.test(toErrorMessage(error));
|
|
14546
|
+
if (isMissingFile) {
|
|
14547
|
+
console.warn(`[MatterportTagCopyService] Orphaned S3 reference, skipping item: "${sourceKey}" (${contextLabel})`);
|
|
14548
|
+
throw new MissingS3FileError(sourceKey, contextLabel);
|
|
14549
|
+
}
|
|
14550
|
+
throw error;
|
|
14551
|
+
}
|
|
14552
|
+
}
|
|
14553
|
+
/**
|
|
14554
|
+
* Duplicates the file referenced by a POI's tagIcon (a JSON string shaped like
|
|
14555
|
+
* { src, opacity }, per form-feature.component.ts). If the JSON can't be parsed or
|
|
14556
|
+
* has no `src`, the value is returned unchanged rather than risk corrupting it.
|
|
14557
|
+
*/
|
|
14558
|
+
async duplicateTagIcon(tagIconRaw) {
|
|
14559
|
+
if (!tagIconRaw) {
|
|
14560
|
+
return tagIconRaw;
|
|
14561
|
+
}
|
|
14562
|
+
try {
|
|
14563
|
+
const parsed = JSON.parse(tagIconRaw);
|
|
14564
|
+
if (parsed && parsed.src) {
|
|
14565
|
+
parsed.src = await this.copyS3KeyIfPresent(parsed.src, 'tag icon');
|
|
14566
|
+
}
|
|
14567
|
+
return JSON.stringify(parsed);
|
|
14568
|
+
}
|
|
14569
|
+
catch {
|
|
14570
|
+
// Not JSON, or unexpected shape — leave untouched rather than guess.
|
|
14571
|
+
return tagIconRaw;
|
|
14572
|
+
}
|
|
14573
|
+
}
|
|
14574
|
+
/**
|
|
14575
|
+
* Duplicates tagImageID if it looks like a reference to a file (contains a path
|
|
14576
|
+
* separator, whether raw key or pre-signed URL). UNCONFIRMED ASSUMPTION: this
|
|
14577
|
+
* field's exact semantics weren't verified — if it's actually an opaque reference
|
|
14578
|
+
* to another DB record rather than a file, this heuristic may incorrectly attempt
|
|
14579
|
+
* a copy. Confirm the real structure to tighten this if it causes issues.
|
|
14580
|
+
*/
|
|
14581
|
+
async duplicateTagImageID(tagImageID) {
|
|
14582
|
+
if (!tagImageID || !tagImageID.includes('/')) {
|
|
14583
|
+
return tagImageID;
|
|
14584
|
+
}
|
|
14585
|
+
return this.copyS3KeyIfPresent(tagImageID, 'tag image');
|
|
14586
|
+
}
|
|
14587
|
+
/**
|
|
14588
|
+
* Extracts and normalizes the raw S3 key from a stored value (which may be a full URL
|
|
14589
|
+
* or already a raw key).
|
|
14590
|
+
* IMPORTANT: this must return the RAW (decoded, non URL-encoded) key. Amplify's
|
|
14591
|
+
* Storage.copy() already builds and encodes the 'x-amz-copy-source' header internally
|
|
14592
|
+
* from the key we give it. Pre-encoding it here caused a double-encoding bug (e.g. a
|
|
14593
|
+
* space became "%2520" instead of "%20"), so Storage.copy() looked for a key that never
|
|
14594
|
+
* existed on S3, resulting in a NoSuchKey error.
|
|
14595
|
+
*/
|
|
14596
|
+
extractS3KeyFromValue(value) {
|
|
14597
|
+
if (!value) {
|
|
14598
|
+
return '';
|
|
14599
|
+
}
|
|
14600
|
+
let extracted = value;
|
|
14601
|
+
// 1. Extract path from URL and drop query params
|
|
14602
|
+
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
14603
|
+
try {
|
|
14604
|
+
const url = new URL(value);
|
|
14605
|
+
extracted = decodeURIComponent(url.pathname);
|
|
14606
|
+
}
|
|
14607
|
+
catch {
|
|
14608
|
+
extracted = value.split('?')[0];
|
|
14609
|
+
}
|
|
14610
|
+
}
|
|
14611
|
+
else {
|
|
14612
|
+
extracted = value.split('?')[0];
|
|
14613
|
+
}
|
|
14614
|
+
// 2. Remove leading slash
|
|
14615
|
+
if (extracted.startsWith('/')) {
|
|
14616
|
+
extracted = extracted.substring(1);
|
|
14617
|
+
}
|
|
14618
|
+
// 3. Remove 'public/' prefix
|
|
14619
|
+
// Amplify prepends this automatically; we must strip it from the input
|
|
14620
|
+
// to avoid the 'public/public/' double-prefix issue.
|
|
14621
|
+
if (extracted.startsWith('public/')) {
|
|
14622
|
+
extracted = extracted.substring('public/'.length);
|
|
14623
|
+
}
|
|
14624
|
+
// Return the raw key as-is — Storage.copy() takes care of properly encoding it
|
|
14625
|
+
// for the CopySource header. Do NOT re-encode it here.
|
|
14626
|
+
return extracted;
|
|
14627
|
+
}
|
|
14628
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportTagCopyService, deps: [{ token: NavigationService }, { token: BaseUserService }, { token: PoiService }, { token: TicketsService }, { token: EquipmentService }, { token: Object3DService }, { token: FeatureService }, { token: MeasurementService }, { token: CommentService }, { token: i1.TranslateService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
14629
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportTagCopyService, providedIn: 'root' });
|
|
14630
|
+
}
|
|
14631
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MatterportTagCopyService, decorators: [{
|
|
14632
|
+
type: Injectable,
|
|
14633
|
+
args: [{ providedIn: 'root' }]
|
|
14634
|
+
}], ctorParameters: () => [{ type: NavigationService }, { type: BaseUserService }, { type: PoiService }, { type: TicketsService }, { type: EquipmentService }, { type: Object3DService }, { type: FeatureService }, { type: MeasurementService }, { type: CommentService }, { type: i1.TranslateService }] });
|
|
14635
|
+
|
|
13487
14636
|
/* eslint-disable class-methods-use-this */
|
|
13488
14637
|
class OrganisationService {
|
|
13489
14638
|
smarterPlanID = "6e28201f-4679-4c29-9d2d-8a325f57cee9";
|
|
@@ -13691,5 +14840,5 @@ function floatValidator() {
|
|
|
13691
14840
|
* Generated bundle index. Do not edit.
|
|
13692
14841
|
*/
|
|
13693
14842
|
|
|
13694
|
-
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, MatterportImportService, MatterportService, MattertagActionMode, MattertagData, MeasurementService, MenuBarComponent, MissionService, NavigationService, NavigatorService, NgxSmarterplanCoreModule, NgxSmarterplanCoreService, NodeService, Object3DService, OperationService, OrganisationService, PaymentStatus, PlanService, PoiService, PoiType, ProfileEntity, ProfileService, ProfileStatus, PropertyService, PropertyType, RoleStatus, SafeUrlPipe, SearchBarComponent, SearchObjectType, SearchService, SpModule, SpaceService, SpaceStatus, StatusEquipment, SupportModalComponent, SupportService, TagAction, TemplateService, TicketPriority, TicketStatus, TicketType, TicketsService, TimeDateToLocalStringPipe, TypeNote, UsernameFromIdPipe, ValidatorsService, ViewerInteractions, ViewerService, VisitService, ZoneChangeService, ZoneService, arraysContainSameElements, checkElementById, convertTimestampToLocalZone, dateHasExpired, dateTimeToLocalString, deleteFromS3, downloadBlob, downloadEquipmentDocument, downloadFile, downloadFileAsObject, durationToString, emailValidator, enumToArray, filterUniqueArrayByID, floatValidator, getBufferForFileFromS3, getCoefficientsForImage, getCurrentLang, getDistanceBetweenTwoPoints, getHighestLevelForMissions, getHighestRoleForMissions, getLevelsBelow, getLocaleLong, getLocaleShort, getMetaForImage, getRolesBelowForManager, getSignedFile, getSignedImageUrlForEquipment, getSignedImageUrlForProfile, getSignedImageUrlForSpace, getSpaceIDFromUrl, getVisitUrl, isEmptyObject, listFilesInFolder, mean, noEmptyValidator, numberToDateString, openDocument, openModalForVisitSwitch, poiTypeToString, pointToString, removeAllFilesFromFolderS3, removeNullKeysFromObject, showScanPointsOnPlanInDiv, shuffleArray, sortAlphabeticallyOnName, stringLocaleToEnum, stringToLowercaseNoSpaces, styleButton, textValidator, translateDatePeriod, uploadBase64Image, uploadBase64ImageWithRetry, uploadFileToS3, uploadJsonToS3, validEmail, wait, waitUntil };
|
|
14843
|
+
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, MattertagActionMode, MattertagData, MeasurementService, MenuBarComponent, MissionService, NavigationService, NavigatorService, NgxSmarterplanCoreModule, NgxSmarterplanCoreService, NodeService, Object3DService, OperationService, OrganisationService, PaymentStatus, PlanService, PoiService, PoiType, ProfileEntity, ProfileService, ProfileStatus, PropertyService, PropertyType, RoleStatus, SafeUrlPipe, SearchBarComponent, SearchObjectType, SearchService, SpModule, SpaceService, SpaceStatus, StatusEquipment, SupportModalComponent, SupportService, TagAction, TemplateService, TicketPriority, TicketStatus, TicketType, TicketsService, TimeDateToLocalStringPipe, TypeNote, UsernameFromIdPipe, ValidatorsService, ViewerInteractions, ViewerService, VisitService, ZoneChangeService, ZoneService, applyRigidTransformToPoint, applyRigidTransformToYRotationDeg, arraysContainSameElements, checkElementById, computeRigidTransform, convertTimestampToLocalZone, copyFileInS3, dateHasExpired, dateTimeToLocalString, deleteFromS3, distance, downloadBlob, downloadEquipmentDocument, downloadFile, downloadFileAsObject, durationToString, emailValidator, enumToArray, filterUniqueArrayByID, floatValidator, getBufferForFileFromS3, getCoefficientsForImage, getCurrentLang, getDistanceBetweenTwoPoints, getHighestLevelForMissions, getHighestRoleForMissions, getLevelsBelow, getLocaleLong, getLocaleShort, getMetaForImage, getRolesBelowForManager, getSignedFile, getSignedImageUrlForEquipment, getSignedImageUrlForProfile, getSignedImageUrlForSpace, getSpaceIDFromUrl, getVisitUrl, isEmptyObject, listFilesInFolder, mean, noEmptyValidator, numberToDateString, openDocument, openModalForVisitSwitch, poiTypeToString, pointToString, removeAllFilesFromFolderS3, removeNullKeysFromObject, showScanPointsOnPlanInDiv, shuffleArray, sortAlphabeticallyOnName, stringLocaleToEnum, stringToLowercaseNoSpaces, styleButton, textValidator, translateDatePeriod, uploadBase64Image, uploadBase64ImageWithRetry, uploadFileToS3, uploadJsonToS3, validEmail, wait, waitUntil };
|
|
13695
14844
|
//# sourceMappingURL=smarterplan-ngx-smarterplan-core.mjs.map
|