@blueharford/scrypted-spatial-awareness 0.2.1 → 0.3.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.
@@ -105,6 +105,23 @@ export const EDITOR_HTML = `<!DOCTYPE html>
105
105
  </div>
106
106
  <div id="suggestions-list"></div>
107
107
  </div>
108
+ <div class="section" id="connection-suggestions-section" style="display: none;">
109
+ <div class="section-title">
110
+ <span>Connection Suggestions</span>
111
+ <button class="btn btn-small" onclick="loadConnectionSuggestions()">Refresh</button>
112
+ </div>
113
+ <div id="connection-suggestions-list"></div>
114
+ </div>
115
+ <div class="section" id="live-tracking-section">
116
+ <div class="section-title">
117
+ <span>Live Tracking</span>
118
+ <label class="checkbox-group" style="font-size: 11px; font-weight: normal; text-transform: none;">
119
+ <input type="checkbox" id="live-tracking-toggle" onchange="toggleLiveTracking(this.checked)">
120
+ Enable
121
+ </label>
122
+ </div>
123
+ <div id="live-tracking-list" style="max-height: 150px; overflow-y: auto;"></div>
124
+ </div>
108
125
  </div>
109
126
  </div>
110
127
  <div class="editor">
@@ -295,6 +312,12 @@ export const EDITOR_HTML = `<!DOCTYPE html>
295
312
  let availableCameras = [];
296
313
  let landmarkTemplates = [];
297
314
  let pendingSuggestions = [];
315
+ let connectionSuggestions = [];
316
+ let liveTrackingData = { objects: [], timestamp: 0 };
317
+ let liveTrackingEnabled = false;
318
+ let liveTrackingInterval = null;
319
+ let selectedJourneyId = null;
320
+ let journeyPath = null;
298
321
  let isDrawing = false;
299
322
  let drawStart = null;
300
323
  let currentDrawing = null;
@@ -307,6 +330,7 @@ export const EDITOR_HTML = `<!DOCTYPE html>
307
330
  await loadAvailableCameras();
308
331
  await loadLandmarkTemplates();
309
332
  await loadSuggestions();
333
+ await loadConnectionSuggestions();
310
334
  resizeCanvas();
311
335
  render();
312
336
  updateUI();
@@ -406,6 +430,132 @@ export const EDITOR_HTML = `<!DOCTYPE html>
406
430
  } catch (e) { console.error('Failed to reject suggestion:', e); }
407
431
  }
408
432
 
433
+ // ==================== Connection Suggestions ====================
434
+ async function loadConnectionSuggestions() {
435
+ try {
436
+ const response = await fetch('../api/connection-suggestions');
437
+ if (response.ok) {
438
+ const data = await response.json();
439
+ connectionSuggestions = data.suggestions || [];
440
+ updateConnectionSuggestionsUI();
441
+ }
442
+ } catch (e) { console.error('Failed to load connection suggestions:', e); }
443
+ }
444
+
445
+ function updateConnectionSuggestionsUI() {
446
+ const section = document.getElementById('connection-suggestions-section');
447
+ const list = document.getElementById('connection-suggestions-list');
448
+ if (connectionSuggestions.length === 0) {
449
+ section.style.display = 'none';
450
+ return;
451
+ }
452
+ section.style.display = 'block';
453
+ list.innerHTML = connectionSuggestions.map(s =>
454
+ '<div class="camera-item" style="display: flex; justify-content: space-between; align-items: center;">' +
455
+ '<div><div class="camera-name">' + s.fromCameraName + ' → ' + s.toCameraName + '</div>' +
456
+ '<div class="camera-info">' + Math.round(s.suggestedTransitTime.typical / 1000) + 's typical, ' +
457
+ Math.round(s.confidence * 100) + '% confidence</div></div>' +
458
+ '<div style="display: flex; gap: 5px;">' +
459
+ '<button class="btn btn-small btn-primary" onclick="acceptConnectionSuggestion(\\'' + s.id + '\\')">Accept</button>' +
460
+ '<button class="btn btn-small" onclick="rejectConnectionSuggestion(\\'' + s.id + '\\')">Reject</button>' +
461
+ '</div></div>'
462
+ ).join('');
463
+ }
464
+
465
+ async function acceptConnectionSuggestion(id) {
466
+ try {
467
+ const response = await fetch('../api/connection-suggestions/' + encodeURIComponent(id) + '/accept', { method: 'POST' });
468
+ if (response.ok) {
469
+ const data = await response.json();
470
+ if (data.connection) {
471
+ topology.connections.push(data.connection);
472
+ updateUI();
473
+ render();
474
+ }
475
+ await loadConnectionSuggestions();
476
+ setStatus('Connection accepted', 'success');
477
+ }
478
+ } catch (e) { console.error('Failed to accept connection suggestion:', e); }
479
+ }
480
+
481
+ async function rejectConnectionSuggestion(id) {
482
+ try {
483
+ await fetch('../api/connection-suggestions/' + encodeURIComponent(id) + '/reject', { method: 'POST' });
484
+ await loadConnectionSuggestions();
485
+ setStatus('Connection suggestion rejected', 'success');
486
+ } catch (e) { console.error('Failed to reject connection suggestion:', e); }
487
+ }
488
+
489
+ // ==================== Live Tracking ====================
490
+ function toggleLiveTracking(enabled) {
491
+ liveTrackingEnabled = enabled;
492
+ if (enabled) {
493
+ loadLiveTracking();
494
+ liveTrackingInterval = setInterval(loadLiveTracking, 2000); // Poll every 2 seconds
495
+ } else {
496
+ if (liveTrackingInterval) {
497
+ clearInterval(liveTrackingInterval);
498
+ liveTrackingInterval = null;
499
+ }
500
+ liveTrackingData = { objects: [], timestamp: 0 };
501
+ selectedJourneyId = null;
502
+ journeyPath = null;
503
+ updateLiveTrackingUI();
504
+ render();
505
+ }
506
+ }
507
+
508
+ async function loadLiveTracking() {
509
+ try {
510
+ const response = await fetch('../api/live-tracking');
511
+ if (response.ok) {
512
+ liveTrackingData = await response.json();
513
+ updateLiveTrackingUI();
514
+ render();
515
+ }
516
+ } catch (e) { console.error('Failed to load live tracking:', e); }
517
+ }
518
+
519
+ function updateLiveTrackingUI() {
520
+ const list = document.getElementById('live-tracking-list');
521
+ if (liveTrackingData.objects.length === 0) {
522
+ list.innerHTML = '<div style="color: #666; font-size: 12px; text-align: center; padding: 10px;">No active objects</div>';
523
+ return;
524
+ }
525
+ list.innerHTML = liveTrackingData.objects.map(obj => {
526
+ const isSelected = selectedJourneyId === obj.globalId;
527
+ const ageSeconds = Math.round((Date.now() - obj.lastSeen) / 1000);
528
+ const ageStr = ageSeconds < 60 ? ageSeconds + 's ago' : Math.round(ageSeconds / 60) + 'm ago';
529
+ return '<div class="camera-item' + (isSelected ? ' selected' : '') + '" ' +
530
+ 'onclick="selectTrackedObject(\\'' + obj.globalId + '\\')" ' +
531
+ 'style="padding: 8px; cursor: pointer;">' +
532
+ '<div class="camera-name" style="font-size: 12px;">' +
533
+ (obj.className.charAt(0).toUpperCase() + obj.className.slice(1)) +
534
+ (obj.label ? ' (' + obj.label + ')' : '') + '</div>' +
535
+ '<div class="camera-info">' + obj.lastCameraName + ' • ' + ageStr + '</div>' +
536
+ '</div>';
537
+ }).join('');
538
+ }
539
+
540
+ async function selectTrackedObject(globalId) {
541
+ if (selectedJourneyId === globalId) {
542
+ // Deselect
543
+ selectedJourneyId = null;
544
+ journeyPath = null;
545
+ } else {
546
+ selectedJourneyId = globalId;
547
+ // Load journey path
548
+ try {
549
+ const response = await fetch('../api/journey-path/' + globalId);
550
+ if (response.ok) {
551
+ journeyPath = await response.json();
552
+ }
553
+ } catch (e) { console.error('Failed to load journey path:', e); }
554
+ }
555
+ updateLiveTrackingUI();
556
+ render();
557
+ }
558
+
409
559
  function openAddLandmarkModal() {
410
560
  updateLandmarkSuggestions();
411
561
  document.getElementById('add-landmark-modal').classList.add('active');
@@ -629,6 +779,112 @@ export const EDITOR_HTML = `<!DOCTYPE html>
629
779
  for (const camera of topology.cameras) {
630
780
  if (camera.floorPlanPosition) { drawCamera(camera); }
631
781
  }
782
+
783
+ // Draw journey path if selected
784
+ if (journeyPath && journeyPath.segments.length > 0) {
785
+ drawJourneyPath();
786
+ }
787
+
788
+ // Draw live tracking objects
789
+ if (liveTrackingEnabled && liveTrackingData.objects.length > 0) {
790
+ drawLiveTrackingObjects();
791
+ }
792
+ }
793
+
794
+ function drawJourneyPath() {
795
+ if (!journeyPath) return;
796
+
797
+ ctx.strokeStyle = '#ff6b6b';
798
+ ctx.lineWidth = 3;
799
+ ctx.setLineDash([8, 4]);
800
+
801
+ // Draw path segments
802
+ for (const segment of journeyPath.segments) {
803
+ if (segment.fromCamera.position && segment.toCamera.position) {
804
+ ctx.beginPath();
805
+ ctx.moveTo(segment.fromCamera.position.x, segment.fromCamera.position.y);
806
+ ctx.lineTo(segment.toCamera.position.x, segment.toCamera.position.y);
807
+ ctx.stroke();
808
+
809
+ // Draw timestamp indicator
810
+ const midX = (segment.fromCamera.position.x + segment.toCamera.position.x) / 2;
811
+ const midY = (segment.fromCamera.position.y + segment.toCamera.position.y) / 2;
812
+ ctx.fillStyle = 'rgba(255, 107, 107, 0.9)';
813
+ ctx.beginPath();
814
+ ctx.arc(midX, midY, 4, 0, Math.PI * 2);
815
+ ctx.fill();
816
+ }
817
+ }
818
+
819
+ ctx.setLineDash([]);
820
+
821
+ // Draw current location indicator
822
+ if (journeyPath.currentLocation?.position) {
823
+ const pos = journeyPath.currentLocation.position;
824
+ // Pulsing dot effect
825
+ const pulse = (Date.now() % 1000) / 1000;
826
+ const radius = 10 + pulse * 5;
827
+ const alpha = 1 - pulse * 0.5;
828
+
829
+ ctx.beginPath();
830
+ ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2);
831
+ ctx.fillStyle = 'rgba(255, 107, 107, ' + alpha + ')';
832
+ ctx.fill();
833
+
834
+ ctx.beginPath();
835
+ ctx.arc(pos.x, pos.y, 6, 0, Math.PI * 2);
836
+ ctx.fillStyle = '#ff6b6b';
837
+ ctx.fill();
838
+ ctx.strokeStyle = '#fff';
839
+ ctx.lineWidth = 2;
840
+ ctx.stroke();
841
+ }
842
+ }
843
+
844
+ function drawLiveTrackingObjects() {
845
+ const objectColors = {
846
+ person: '#4caf50',
847
+ car: '#2196f3',
848
+ animal: '#ff9800',
849
+ default: '#9c27b0'
850
+ };
851
+
852
+ for (const obj of liveTrackingData.objects) {
853
+ if (!obj.cameraPosition) continue;
854
+
855
+ // Skip if this is the selected journey object (drawn separately with path)
856
+ if (obj.globalId === selectedJourneyId) continue;
857
+
858
+ const pos = obj.cameraPosition;
859
+ const color = objectColors[obj.className] || objectColors.default;
860
+ const ageSeconds = (Date.now() - obj.lastSeen) / 1000;
861
+
862
+ // Fade old objects
863
+ const alpha = Math.max(0.3, 1 - ageSeconds / 60);
864
+
865
+ // Draw object indicator
866
+ ctx.beginPath();
867
+ ctx.arc(pos.x, pos.y, 12, 0, Math.PI * 2);
868
+ ctx.fillStyle = color.replace(')', ', ' + alpha + ')').replace('rgb', 'rgba');
869
+ ctx.fill();
870
+ ctx.strokeStyle = 'rgba(255, 255, 255, ' + alpha + ')';
871
+ ctx.lineWidth = 2;
872
+ ctx.stroke();
873
+
874
+ // Draw class icon
875
+ ctx.fillStyle = 'rgba(255, 255, 255, ' + alpha + ')';
876
+ ctx.font = 'bold 10px sans-serif';
877
+ ctx.textAlign = 'center';
878
+ ctx.textBaseline = 'middle';
879
+ const icon = obj.className === 'person' ? 'P' : obj.className === 'car' ? 'C' : obj.className === 'animal' ? 'A' : '?';
880
+ ctx.fillText(icon, pos.x, pos.y);
881
+
882
+ // Draw label below
883
+ if (obj.label) {
884
+ ctx.font = '9px sans-serif';
885
+ ctx.fillText(obj.label.slice(0, 10), pos.x, pos.y + 20);
886
+ }
887
+ }
632
888
  }
633
889
 
634
890
  function drawLandmark(landmark) {