@blueharford/scrypted-spatial-awareness 0.1.16 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -35
- package/dist/main.nodejs.js +1 -1
- package/dist/main.nodejs.js.map +1 -1
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +1443 -57
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/spatial-reasoning.ts +700 -0
- package/src/core/tracking-engine.ts +137 -53
- package/src/main.ts +266 -3
- package/src/models/alert.ts +21 -1
- package/src/models/topology.ts +382 -9
- package/src/ui/editor-html.ts +328 -6
package/src/models/topology.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Camera Topology Models
|
|
3
|
-
* Defines the spatial relationships between cameras
|
|
3
|
+
* Defines the spatial relationships between cameras, landmarks, and zones
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/** A point in 2D space (normalized 0-100 or pixel coordinates) */
|
|
@@ -15,14 +15,106 @@ export interface FloorPlanPosition {
|
|
|
15
15
|
y: number;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// ==================== Landmark Types ====================
|
|
19
|
+
|
|
20
|
+
/** Types of landmarks in the topology */
|
|
21
|
+
export type LandmarkType =
|
|
22
|
+
| 'structure' // House, shed, garage, porch
|
|
23
|
+
| 'feature' // Mailbox, tree, firepit, deck, pool
|
|
24
|
+
| 'boundary' // Fence, wall, hedge, property line
|
|
25
|
+
| 'access' // Driveway, walkway, gate, door, stairs
|
|
26
|
+
| 'vehicle' // Parked car location, boat, RV
|
|
27
|
+
| 'neighbor' // Neighbor's house, neighbor's driveway
|
|
28
|
+
| 'zone' // Front yard, back yard, side yard
|
|
29
|
+
| 'street'; // Street, sidewalk, alley
|
|
30
|
+
|
|
31
|
+
/** Common landmark templates for quick setup */
|
|
32
|
+
export const LANDMARK_TEMPLATES: { type: LandmarkType; suggestions: string[] }[] = [
|
|
33
|
+
{ type: 'structure', suggestions: ['House', 'Garage', 'Shed', 'Porch', 'Deck', 'Patio', 'Gazebo', 'Pool House'] },
|
|
34
|
+
{ type: 'feature', suggestions: ['Mailbox', 'Tree', 'Firepit', 'Pool', 'Hot Tub', 'Garden', 'Fountain', 'Flagpole'] },
|
|
35
|
+
{ type: 'boundary', suggestions: ['Front Fence', 'Back Fence', 'Side Fence', 'Hedge', 'Wall', 'Property Line'] },
|
|
36
|
+
{ type: 'access', suggestions: ['Driveway', 'Front Walkway', 'Back Walkway', 'Front Door', 'Back Door', 'Side Door', 'Gate', 'Stairs'] },
|
|
37
|
+
{ type: 'vehicle', suggestions: ['Car Parking', 'Boat', 'RV Pad', 'Motorcycle Spot'] },
|
|
38
|
+
{ type: 'neighbor', suggestions: ["Neighbor's House", "Neighbor's Driveway", "Neighbor's Yard"] },
|
|
39
|
+
{ type: 'zone', suggestions: ['Front Yard', 'Back Yard', 'Side Yard', 'Courtyard'] },
|
|
40
|
+
{ type: 'street', suggestions: ['Street', 'Sidewalk', 'Alley', 'Cul-de-sac'] },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** A landmark/static object in the topology */
|
|
44
|
+
export interface Landmark {
|
|
45
|
+
/** Unique identifier */
|
|
46
|
+
id: string;
|
|
47
|
+
/** Display name (e.g., "Mailbox", "Red Shed") */
|
|
48
|
+
name: string;
|
|
49
|
+
/** Type of landmark */
|
|
50
|
+
type: LandmarkType;
|
|
51
|
+
/** Position on floor plan */
|
|
52
|
+
position: FloorPlanPosition;
|
|
53
|
+
/** Optional polygon outline on floor plan */
|
|
54
|
+
outline?: ClipPath;
|
|
55
|
+
/** Human-readable description for LLM context */
|
|
56
|
+
description?: string;
|
|
57
|
+
/** Can someone enter property through/near this landmark? */
|
|
58
|
+
isEntryPoint?: boolean;
|
|
59
|
+
/** Can someone exit property through/near this landmark? */
|
|
60
|
+
isExitPoint?: boolean;
|
|
61
|
+
/** IDs of adjacent landmarks (for path calculation) */
|
|
62
|
+
adjacentTo?: string[];
|
|
63
|
+
/** IDs of cameras that can see this landmark */
|
|
64
|
+
visibleFromCameras?: string[];
|
|
65
|
+
/** Whether this was suggested by AI (pending user confirmation) */
|
|
66
|
+
aiSuggested?: boolean;
|
|
67
|
+
/** Confidence score if AI suggested (0-1) */
|
|
68
|
+
aiConfidence?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ==================== Camera FOV Types ====================
|
|
72
|
+
|
|
73
|
+
/** Camera field of view - simple configuration */
|
|
74
|
+
export interface CameraFOVSimple {
|
|
75
|
+
mode: 'simple';
|
|
76
|
+
/** FOV angle in degrees (e.g., 90, 120) */
|
|
77
|
+
angle: number;
|
|
78
|
+
/** Direction the camera faces in degrees (0 = up/north on floor plan, 90 = right/east) */
|
|
79
|
+
direction: number;
|
|
80
|
+
/** How far the camera can see (in floor plan units) */
|
|
81
|
+
range: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Camera field of view - polygon configuration */
|
|
85
|
+
export interface CameraFOVPolygon {
|
|
86
|
+
mode: 'polygon';
|
|
87
|
+
/** Polygon defining exact coverage area on floor plan */
|
|
88
|
+
polygon: ClipPath;
|
|
89
|
+
}
|
|
90
|
+
|
|
18
91
|
/** Camera field of view configuration */
|
|
19
|
-
export
|
|
20
|
-
|
|
92
|
+
export type CameraFOV = CameraFOVSimple | CameraFOVPolygon;
|
|
93
|
+
|
|
94
|
+
/** Legacy FOV format for backward compatibility */
|
|
95
|
+
export interface CameraFOVLegacy {
|
|
21
96
|
angle: number;
|
|
22
|
-
/** Direction the camera faces in degrees from north (0 = north, 90 = east) */
|
|
23
97
|
direction: number;
|
|
24
98
|
}
|
|
25
99
|
|
|
100
|
+
// ==================== Camera Context ====================
|
|
101
|
+
|
|
102
|
+
/** Rich context description for a camera */
|
|
103
|
+
export interface CameraContext {
|
|
104
|
+
/** Where the camera is mounted (e.g., "Under front porch awning", "On garage wall") */
|
|
105
|
+
mountLocation?: string;
|
|
106
|
+
/** What the camera is pointing at / what it can see */
|
|
107
|
+
description?: string;
|
|
108
|
+
/** Height of camera mount in feet (helps with perspective understanding) */
|
|
109
|
+
mountHeight?: number;
|
|
110
|
+
/** IDs of landmarks visible from this camera */
|
|
111
|
+
visibleLandmarks?: string[];
|
|
112
|
+
/** Natural language description of camera coverage for LLM */
|
|
113
|
+
coverageDescription?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ==================== Transit Configuration ====================
|
|
117
|
+
|
|
26
118
|
/** Transit time configuration between cameras */
|
|
27
119
|
export interface TransitTime {
|
|
28
120
|
/** Minimum expected transit time in milliseconds */
|
|
@@ -33,6 +125,8 @@ export interface TransitTime {
|
|
|
33
125
|
max: number;
|
|
34
126
|
}
|
|
35
127
|
|
|
128
|
+
// ==================== Camera Node ====================
|
|
129
|
+
|
|
36
130
|
/** Represents a camera in the topology */
|
|
37
131
|
export interface CameraNode {
|
|
38
132
|
/** Scrypted device ID */
|
|
@@ -44,15 +138,19 @@ export interface CameraNode {
|
|
|
44
138
|
/** Position on floor plan (optional) */
|
|
45
139
|
floorPlanPosition?: FloorPlanPosition;
|
|
46
140
|
/** Camera field of view configuration (optional) */
|
|
47
|
-
fov?: CameraFOV;
|
|
141
|
+
fov?: CameraFOV | CameraFOVLegacy;
|
|
48
142
|
/** Is this an entry point to the property */
|
|
49
143
|
isEntryPoint: boolean;
|
|
50
144
|
/** Is this an exit point from the property */
|
|
51
145
|
isExitPoint: boolean;
|
|
52
146
|
/** Detection classes to track on this camera */
|
|
53
147
|
trackClasses: string[];
|
|
148
|
+
/** Rich context description */
|
|
149
|
+
context?: CameraContext;
|
|
54
150
|
}
|
|
55
151
|
|
|
152
|
+
// ==================== Connections ====================
|
|
153
|
+
|
|
56
154
|
/** Represents a connection between two cameras */
|
|
57
155
|
export interface CameraConnection {
|
|
58
156
|
/** Unique identifier for this connection */
|
|
@@ -71,8 +169,12 @@ export interface CameraConnection {
|
|
|
71
169
|
bidirectional: boolean;
|
|
72
170
|
/** Human-readable path name (e.g., "Driveway to Front Door") */
|
|
73
171
|
name: string;
|
|
172
|
+
/** Landmarks along this path (for rich descriptions) */
|
|
173
|
+
pathLandmarks?: string[];
|
|
74
174
|
}
|
|
75
175
|
|
|
176
|
+
// ==================== Zones ====================
|
|
177
|
+
|
|
76
178
|
/** Zone type for alerting purposes */
|
|
77
179
|
export type GlobalZoneType = 'entry' | 'exit' | 'dwell' | 'restricted';
|
|
78
180
|
|
|
@@ -96,6 +198,55 @@ export interface CameraZoneMapping {
|
|
|
96
198
|
zone: ClipPath;
|
|
97
199
|
}
|
|
98
200
|
|
|
201
|
+
// ==================== Spatial Relationships ====================
|
|
202
|
+
|
|
203
|
+
/** Types of spatial relationships between entities */
|
|
204
|
+
export type RelationshipType =
|
|
205
|
+
| 'adjacent' // Next to each other
|
|
206
|
+
| 'leads_to' // Path from A leads to B
|
|
207
|
+
| 'visible_from' // A is visible from B
|
|
208
|
+
| 'part_of' // A is part of B (e.g., front door is part of house)
|
|
209
|
+
| 'contains' // A contains B
|
|
210
|
+
| 'near' // Close proximity
|
|
211
|
+
| 'across_from' // Opposite sides
|
|
212
|
+
| 'between'; // A is between B and C
|
|
213
|
+
|
|
214
|
+
/** A spatial relationship between two entities */
|
|
215
|
+
export interface SpatialRelationship {
|
|
216
|
+
/** Unique identifier */
|
|
217
|
+
id: string;
|
|
218
|
+
/** Type of relationship */
|
|
219
|
+
type: RelationshipType;
|
|
220
|
+
/** First entity (camera ID or landmark ID) */
|
|
221
|
+
entityA: string;
|
|
222
|
+
/** Second entity (camera ID or landmark ID) */
|
|
223
|
+
entityB: string;
|
|
224
|
+
/** Optional third entity for 'between' relationships */
|
|
225
|
+
entityC?: string;
|
|
226
|
+
/** Optional description */
|
|
227
|
+
description?: string;
|
|
228
|
+
/** Whether this was auto-inferred */
|
|
229
|
+
autoInferred?: boolean;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ==================== Property Configuration ====================
|
|
233
|
+
|
|
234
|
+
/** Overall property description for context */
|
|
235
|
+
export interface PropertyConfig {
|
|
236
|
+
/** Type of property */
|
|
237
|
+
propertyType?: 'single_family' | 'townhouse' | 'apartment' | 'condo' | 'commercial' | 'other';
|
|
238
|
+
/** Description of the property for LLM context */
|
|
239
|
+
description?: string;
|
|
240
|
+
/** Address (optional, for context) */
|
|
241
|
+
address?: string;
|
|
242
|
+
/** Lot features */
|
|
243
|
+
features?: string[];
|
|
244
|
+
/** Cardinal direction the front of the property faces */
|
|
245
|
+
frontFacing?: 'north' | 'south' | 'east' | 'west' | 'northeast' | 'northwest' | 'southeast' | 'southwest';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ==================== Floor Plan ====================
|
|
249
|
+
|
|
99
250
|
/** Floor plan image configuration */
|
|
100
251
|
export interface FloorPlanConfig {
|
|
101
252
|
/** Base64 encoded image data or URL */
|
|
@@ -104,30 +255,64 @@ export interface FloorPlanConfig {
|
|
|
104
255
|
width: number;
|
|
105
256
|
/** Image height in pixels */
|
|
106
257
|
height: number;
|
|
107
|
-
/** Scale factor (pixels per real-world
|
|
258
|
+
/** Scale factor (pixels per real-world foot, for distance calculations) */
|
|
108
259
|
scale?: number;
|
|
260
|
+
/** Rotation of the floor plan (0 = north is up) */
|
|
261
|
+
rotation?: number;
|
|
109
262
|
}
|
|
110
263
|
|
|
264
|
+
// ==================== AI Suggestions ====================
|
|
265
|
+
|
|
266
|
+
/** An AI-suggested landmark pending user confirmation */
|
|
267
|
+
export interface LandmarkSuggestion {
|
|
268
|
+
/** Unique ID for this suggestion */
|
|
269
|
+
id: string;
|
|
270
|
+
/** Suggested landmark */
|
|
271
|
+
landmark: Landmark;
|
|
272
|
+
/** Which camera(s) detected this */
|
|
273
|
+
detectedByCameras: string[];
|
|
274
|
+
/** When this was suggested */
|
|
275
|
+
timestamp: number;
|
|
276
|
+
/** Detection count (how many times AI identified this) */
|
|
277
|
+
detectionCount: number;
|
|
278
|
+
/** Status */
|
|
279
|
+
status: 'pending' | 'accepted' | 'rejected';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ==================== Complete Topology ====================
|
|
283
|
+
|
|
111
284
|
/** Complete camera topology configuration */
|
|
112
285
|
export interface CameraTopology {
|
|
113
286
|
/** Version for migration support */
|
|
114
287
|
version: string;
|
|
288
|
+
/** Property-level configuration */
|
|
289
|
+
property?: PropertyConfig;
|
|
115
290
|
/** All cameras in the system */
|
|
116
291
|
cameras: CameraNode[];
|
|
117
292
|
/** Connections between cameras */
|
|
118
293
|
connections: CameraConnection[];
|
|
294
|
+
/** Static landmarks/objects */
|
|
295
|
+
landmarks: Landmark[];
|
|
296
|
+
/** Spatial relationships (auto-inferred + manual) */
|
|
297
|
+
relationships: SpatialRelationship[];
|
|
119
298
|
/** Named zones spanning multiple cameras */
|
|
120
299
|
globalZones: GlobalZone[];
|
|
121
300
|
/** Floor plan configuration (optional) */
|
|
122
301
|
floorPlan?: FloorPlanConfig;
|
|
302
|
+
/** Pending AI landmark suggestions */
|
|
303
|
+
pendingSuggestions?: LandmarkSuggestion[];
|
|
123
304
|
}
|
|
124
305
|
|
|
306
|
+
// ==================== Helper Functions ====================
|
|
307
|
+
|
|
125
308
|
/** Creates an empty topology */
|
|
126
309
|
export function createEmptyTopology(): CameraTopology {
|
|
127
310
|
return {
|
|
128
|
-
version: '
|
|
311
|
+
version: '2.0',
|
|
129
312
|
cameras: [],
|
|
130
313
|
connections: [],
|
|
314
|
+
landmarks: [],
|
|
315
|
+
relationships: [],
|
|
131
316
|
globalZones: [],
|
|
132
317
|
};
|
|
133
318
|
}
|
|
@@ -137,6 +322,11 @@ export function findCamera(topology: CameraTopology, deviceId: string): CameraNo
|
|
|
137
322
|
return topology.cameras.find(c => c.deviceId === deviceId);
|
|
138
323
|
}
|
|
139
324
|
|
|
325
|
+
/** Finds a landmark by ID */
|
|
326
|
+
export function findLandmark(topology: CameraTopology, landmarkId: string): Landmark | undefined {
|
|
327
|
+
return topology.landmarks.find(l => l.id === landmarkId);
|
|
328
|
+
}
|
|
329
|
+
|
|
140
330
|
/** Finds connections from a camera */
|
|
141
331
|
export function findConnectionsFrom(topology: CameraTopology, cameraId: string): CameraConnection[] {
|
|
142
332
|
return topology.connections.filter(c =>
|
|
@@ -151,10 +341,10 @@ export function findConnection(
|
|
|
151
341
|
fromCameraId: string,
|
|
152
342
|
toCameraId: string
|
|
153
343
|
): CameraConnection | undefined {
|
|
154
|
-
return topology.connections.
|
|
344
|
+
return topology.connections.filter(c =>
|
|
155
345
|
(c.fromCameraId === fromCameraId && c.toCameraId === toCameraId) ||
|
|
156
346
|
(c.bidirectional && c.fromCameraId === toCameraId && c.toCameraId === fromCameraId)
|
|
157
|
-
);
|
|
347
|
+
)[0];
|
|
158
348
|
}
|
|
159
349
|
|
|
160
350
|
/** Gets all entry point cameras */
|
|
@@ -166,3 +356,186 @@ export function getEntryPoints(topology: CameraTopology): CameraNode[] {
|
|
|
166
356
|
export function getExitPoints(topology: CameraTopology): CameraNode[] {
|
|
167
357
|
return topology.cameras.filter(c => c.isExitPoint);
|
|
168
358
|
}
|
|
359
|
+
|
|
360
|
+
/** Gets landmarks visible from a camera */
|
|
361
|
+
export function getLandmarksVisibleFromCamera(topology: CameraTopology, cameraId: string): Landmark[] {
|
|
362
|
+
const camera = findCamera(topology, cameraId);
|
|
363
|
+
if (!camera?.context?.visibleLandmarks) return [];
|
|
364
|
+
return camera.context.visibleLandmarks
|
|
365
|
+
.map(id => findLandmark(topology, id))
|
|
366
|
+
.filter((l): l is Landmark => l !== undefined);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Gets cameras that can see a landmark */
|
|
370
|
+
export function getCamerasWithLandmarkVisibility(topology: CameraTopology, landmarkId: string): CameraNode[] {
|
|
371
|
+
return topology.cameras.filter(c =>
|
|
372
|
+
c.context?.visibleLandmarks?.includes(landmarkId)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Gets adjacent landmarks */
|
|
377
|
+
export function getAdjacentLandmarks(topology: CameraTopology, landmarkId: string): Landmark[] {
|
|
378
|
+
const landmark = findLandmark(topology, landmarkId);
|
|
379
|
+
if (!landmark?.adjacentTo) return [];
|
|
380
|
+
return landmark.adjacentTo
|
|
381
|
+
.map(id => findLandmark(topology, id))
|
|
382
|
+
.filter((l): l is Landmark => l !== undefined);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Calculates distance between two floor plan positions */
|
|
386
|
+
export function calculateDistance(posA: FloorPlanPosition, posB: FloorPlanPosition): number {
|
|
387
|
+
const dx = posB.x - posA.x;
|
|
388
|
+
const dy = posB.y - posA.y;
|
|
389
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Auto-infers relationships based on positions and proximity */
|
|
393
|
+
export function inferRelationships(topology: CameraTopology, proximityThreshold: number = 50): SpatialRelationship[] {
|
|
394
|
+
const relationships: SpatialRelationship[] = [];
|
|
395
|
+
const entities: { id: string; position: FloorPlanPosition; type: 'camera' | 'landmark' }[] = [];
|
|
396
|
+
|
|
397
|
+
// Collect all positioned entities
|
|
398
|
+
for (const camera of topology.cameras) {
|
|
399
|
+
if (camera.floorPlanPosition) {
|
|
400
|
+
entities.push({ id: camera.deviceId, position: camera.floorPlanPosition, type: 'camera' });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
for (const landmark of topology.landmarks) {
|
|
404
|
+
entities.push({ id: landmark.id, position: landmark.position, type: 'landmark' });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Find adjacent entities based on proximity
|
|
408
|
+
for (let i = 0; i < entities.length; i++) {
|
|
409
|
+
for (let j = i + 1; j < entities.length; j++) {
|
|
410
|
+
const distance = calculateDistance(entities[i].position, entities[j].position);
|
|
411
|
+
if (distance <= proximityThreshold) {
|
|
412
|
+
relationships.push({
|
|
413
|
+
id: `auto_${entities[i].id}_${entities[j].id}`,
|
|
414
|
+
type: distance <= proximityThreshold / 2 ? 'adjacent' : 'near',
|
|
415
|
+
entityA: entities[i].id,
|
|
416
|
+
entityB: entities[j].id,
|
|
417
|
+
autoInferred: true,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return relationships;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Generates a natural language description of the topology for LLM context */
|
|
427
|
+
export function generateTopologyDescription(topology: CameraTopology): string {
|
|
428
|
+
const lines: string[] = [];
|
|
429
|
+
|
|
430
|
+
// Property description
|
|
431
|
+
if (topology.property?.description) {
|
|
432
|
+
lines.push(`Property: ${topology.property.description}`);
|
|
433
|
+
}
|
|
434
|
+
if (topology.property?.frontFacing) {
|
|
435
|
+
lines.push(`Front of property faces ${topology.property.frontFacing}.`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Landmarks
|
|
439
|
+
if (topology.landmarks.length > 0) {
|
|
440
|
+
lines.push('\nLandmarks on property:');
|
|
441
|
+
for (const landmark of topology.landmarks) {
|
|
442
|
+
let desc = `- ${landmark.name} (${landmark.type})`;
|
|
443
|
+
if (landmark.description) desc += `: ${landmark.description}`;
|
|
444
|
+
if (landmark.isEntryPoint) desc += ' [Entry point]';
|
|
445
|
+
if (landmark.isExitPoint) desc += ' [Exit point]';
|
|
446
|
+
lines.push(desc);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Cameras
|
|
451
|
+
if (topology.cameras.length > 0) {
|
|
452
|
+
lines.push('\nCamera coverage:');
|
|
453
|
+
for (const camera of topology.cameras) {
|
|
454
|
+
let desc = `- ${camera.name}`;
|
|
455
|
+
if (camera.context?.mountLocation) desc += ` (mounted at ${camera.context.mountLocation})`;
|
|
456
|
+
if (camera.context?.coverageDescription) desc += `: ${camera.context.coverageDescription}`;
|
|
457
|
+
if (camera.isEntryPoint) desc += ' [Watches entry point]';
|
|
458
|
+
if (camera.isExitPoint) desc += ' [Watches exit point]';
|
|
459
|
+
|
|
460
|
+
// List visible landmarks
|
|
461
|
+
if (camera.context?.visibleLandmarks && camera.context.visibleLandmarks.length > 0) {
|
|
462
|
+
const landmarkNames = camera.context.visibleLandmarks
|
|
463
|
+
.map(id => findLandmark(topology, id)?.name)
|
|
464
|
+
.filter(Boolean);
|
|
465
|
+
if (landmarkNames.length > 0) {
|
|
466
|
+
desc += ` Can see: ${landmarkNames.join(', ')}`;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
lines.push(desc);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Connections/paths
|
|
474
|
+
if (topology.connections.length > 0) {
|
|
475
|
+
lines.push('\nMovement paths:');
|
|
476
|
+
for (const conn of topology.connections) {
|
|
477
|
+
const fromCam = findCamera(topology, conn.fromCameraId);
|
|
478
|
+
const toCam = findCamera(topology, conn.toCameraId);
|
|
479
|
+
if (fromCam && toCam) {
|
|
480
|
+
let desc = `- ${fromCam.name} → ${toCam.name}`;
|
|
481
|
+
if (conn.name) desc += ` (${conn.name})`;
|
|
482
|
+
desc += ` [${conn.transitTime.min / 1000}-${conn.transitTime.max / 1000}s transit]`;
|
|
483
|
+
if (conn.bidirectional) desc += ' [bidirectional]';
|
|
484
|
+
lines.push(desc);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return lines.join('\n');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Generates context for a specific movement between cameras */
|
|
493
|
+
export function generateMovementContext(
|
|
494
|
+
topology: CameraTopology,
|
|
495
|
+
fromCameraId: string,
|
|
496
|
+
toCameraId: string,
|
|
497
|
+
objectClass: string
|
|
498
|
+
): string {
|
|
499
|
+
const fromCamera = findCamera(topology, fromCameraId);
|
|
500
|
+
const toCamera = findCamera(topology, toCameraId);
|
|
501
|
+
const connection = findConnection(topology, fromCameraId, toCameraId);
|
|
502
|
+
|
|
503
|
+
if (!fromCamera || !toCamera) {
|
|
504
|
+
return `${objectClass} moving between cameras`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const lines: string[] = [];
|
|
508
|
+
|
|
509
|
+
// Source context
|
|
510
|
+
lines.push(`Origin: ${fromCamera.name}`);
|
|
511
|
+
if (fromCamera.context?.coverageDescription) {
|
|
512
|
+
lines.push(` Coverage: ${fromCamera.context.coverageDescription}`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Destination context
|
|
516
|
+
lines.push(`Destination: ${toCamera.name}`);
|
|
517
|
+
if (toCamera.context?.coverageDescription) {
|
|
518
|
+
lines.push(` Coverage: ${toCamera.context.coverageDescription}`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Path context
|
|
522
|
+
if (connection) {
|
|
523
|
+
if (connection.name) lines.push(`Path: ${connection.name}`);
|
|
524
|
+
if (connection.pathLandmarks && connection.pathLandmarks.length > 0) {
|
|
525
|
+
const landmarkNames = connection.pathLandmarks
|
|
526
|
+
.map(id => findLandmark(topology, id)?.name)
|
|
527
|
+
.filter(Boolean);
|
|
528
|
+
if (landmarkNames.length > 0) {
|
|
529
|
+
lines.push(`Passing: ${landmarkNames.join(' → ')}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Nearby landmarks at destination
|
|
535
|
+
const destLandmarks = getLandmarksVisibleFromCamera(topology, toCameraId);
|
|
536
|
+
if (destLandmarks.length > 0) {
|
|
537
|
+
lines.push(`Near: ${destLandmarks.map(l => l.name).join(', ')}`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return lines.join('\n');
|
|
541
|
+
}
|