@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.
- package/README.md +83 -9
- 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 +679 -4
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/tracking-engine.ts +376 -10
- package/src/main.ts +175 -1
- package/src/ui/editor-html.ts +256 -0
package/src/ui/editor-html.ts
CHANGED
|
@@ -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) {
|