eeeeelin commited on
Commit
2ba2c67
·
1 Parent(s): 1aedac6

aggregate entry and exit data to kpi card and bar chart

Browse files
app/services/__pycache__/processing_service.cpython-311.pyc CHANGED
Binary files a/app/services/__pycache__/processing_service.cpython-311.pyc and b/app/services/__pycache__/processing_service.cpython-311.pyc differ
 
app/services/processing_service.py CHANGED
@@ -1,7 +1,3 @@
1
- """
2
- Background processing service for video analysis.
3
- Handles video processing in a separate thread and emits real-time updates via SocketIO.
4
- """
5
  import threading
6
  from datetime import datetime
7
  from app import socketio
@@ -12,6 +8,7 @@ from app.config import Config
12
  from app.utils.math_utils import calculate_line_signed_distance
13
 
14
  # Global state to track processing jobs
 
15
  processing_jobs = {}
16
 
17
  class ProcessingJob:
@@ -36,7 +33,8 @@ class ProcessingJob:
36
  'status': self.status,
37
  'progress': self.progress,
38
  'error': self.error,
39
- 'location': self.location
 
40
  }
41
 
42
 
@@ -48,7 +46,13 @@ def start_processing(session_id: str, video_path: str, line_points: list,
48
  Returns immediately after starting the thread.
49
  """
50
  job = ProcessingJob(session_id, video_path, line_points, location, video_start_time, camera_role)
51
- processing_jobs[session_id] = job
 
 
 
 
 
 
52
 
53
  # Start processing in background thread
54
  job.thread = threading.Thread(
@@ -62,10 +66,10 @@ def start_processing(session_id: str, video_path: str, line_points: list,
62
 
63
 
64
  def get_job_status(session_id: str) -> dict:
65
- """Get the status of a processing job"""
66
- job = processing_jobs.get(session_id)
67
- if job:
68
- return job.to_dict()
69
  return None
70
 
71
 
@@ -121,8 +125,6 @@ def _process_with_realtime_updates(video_processor: VideoProcessor, job: Process
121
  from PIL import Image
122
  import supervision as sv
123
  from app.config import Config
124
- from app.models import VehicleEvent
125
- import math
126
 
127
  cap = cv2.VideoCapture(job.video_path)
128
  fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
@@ -130,7 +132,8 @@ def _process_with_realtime_updates(video_processor: VideoProcessor, job: Process
130
 
131
  # Setup output video
132
  fourcc = cv2.VideoWriter_fourcc(*"mp4v")
133
- output_filename = f"{job.session_id}_processed.mp4"
 
134
  output_path = os.path.join(Config.OUTPUT_FOLDER, output_filename)
135
  writer = cv2.VideoWriter(output_path, fourcc, fps,
136
  (Config.FRAME_WIDTH, Config.FRAME_HEIGHT))
@@ -287,6 +290,7 @@ def _process_frame_detections(detections, line_points, track_class, track_last_d
287
 
288
  cls_name = video_processor.class_names[track_class[tracker_id]]
289
 
 
290
  if job.camera_role == 'EXIT':
291
  direction = 'OUT'
292
  else:
@@ -321,7 +325,8 @@ def _emit_progress_update(job: ProcessingJob):
321
  """Emit progress update"""
322
  socketio.emit('processing_progress', {
323
  'session_id': job.session_id,
324
- 'progress': job.progress
 
325
  }, room=job.session_id, namespace='/')
326
 
327
 
@@ -329,7 +334,8 @@ def _emit_vehicle_event(job: ProcessingJob, event):
329
  """Emit a new vehicle detection event"""
330
  socketio.emit('vehicle_event', {
331
  'session_id': job.session_id,
332
- 'event': event.to_dict()
 
333
  }, room=job.session_id, namespace='/')
334
 
335
 
@@ -337,7 +343,8 @@ def _emit_statistics_update(job: ProcessingJob, stats: dict):
337
  """Emit updated statistics"""
338
  socketio.emit('statistics_update', {
339
  'session_id': job.session_id,
340
- 'statistics': stats
 
341
  }, room=job.session_id, namespace='/')
342
 
343
 
@@ -345,7 +352,8 @@ def _emit_processing_complete(job: ProcessingJob, final_stats: dict):
345
  """Emit processing completion event"""
346
  socketio.emit('processing_complete', {
347
  'session_id': job.session_id,
348
- 'statistics': final_stats
 
349
  }, room=job.session_id, namespace='/')
350
 
351
 
@@ -353,5 +361,6 @@ def _emit_error(job: ProcessingJob, error_message: str):
353
  """Emit processing error event"""
354
  socketio.emit('processing_error', {
355
  'session_id': job.session_id,
356
- 'error': error_message
357
- }, room=job.session_id, namespace='/')
 
 
 
 
 
 
1
  import threading
2
  from datetime import datetime
3
  from app import socketio
 
8
  from app.utils.math_utils import calculate_line_signed_distance
9
 
10
  # Global state to track processing jobs
11
+ # Changed to support multiple jobs per session: { session_id: { camera_role: job } }
12
  processing_jobs = {}
13
 
14
  class ProcessingJob:
 
33
  'status': self.status,
34
  'progress': self.progress,
35
  'error': self.error,
36
+ 'location': self.location,
37
+ 'camera_role': self.camera_role # Added camera_role
38
  }
39
 
40
 
 
46
  Returns immediately after starting the thread.
47
  """
48
  job = ProcessingJob(session_id, video_path, line_points, location, video_start_time, camera_role)
49
+
50
+ # Initialize session dict if it doesn't exist
51
+ if session_id not in processing_jobs:
52
+ processing_jobs[session_id] = {}
53
+
54
+ # Store job by camera role
55
+ processing_jobs[session_id][camera_role] = job
56
 
57
  # Start processing in background thread
58
  job.thread = threading.Thread(
 
66
 
67
 
68
  def get_job_status(session_id: str) -> dict:
69
+ """Get the status of processing jobs for a session"""
70
+ if session_id in processing_jobs:
71
+ # Return dict of jobs: {'ENTRY': {...}, 'EXIT': {...}}
72
+ return {role: job.to_dict() for role, job in processing_jobs[session_id].items()}
73
  return None
74
 
75
 
 
125
  from PIL import Image
126
  import supervision as sv
127
  from app.config import Config
 
 
128
 
129
  cap = cv2.VideoCapture(job.video_path)
130
  fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
 
132
 
133
  # Setup output video
134
  fourcc = cv2.VideoWriter_fourcc(*"mp4v")
135
+ # Make filename unique per camera
136
+ output_filename = f"{job.session_id}_{job.camera_role}_processed.mp4"
137
  output_path = os.path.join(Config.OUTPUT_FOLDER, output_filename)
138
  writer = cv2.VideoWriter(output_path, fourcc, fps,
139
  (Config.FRAME_WIDTH, Config.FRAME_HEIGHT))
 
290
 
291
  cls_name = video_processor.class_names[track_class[tracker_id]]
292
 
293
+ # Determine direction based on camera role
294
  if job.camera_role == 'EXIT':
295
  direction = 'OUT'
296
  else:
 
325
  """Emit progress update"""
326
  socketio.emit('processing_progress', {
327
  'session_id': job.session_id,
328
+ 'progress': job.progress,
329
+ 'camera_role': job.camera_role
330
  }, room=job.session_id, namespace='/')
331
 
332
 
 
334
  """Emit a new vehicle detection event"""
335
  socketio.emit('vehicle_event', {
336
  'session_id': job.session_id,
337
+ 'event': event.to_dict(),
338
+ 'camera_role': job.camera_role
339
  }, room=job.session_id, namespace='/')
340
 
341
 
 
343
  """Emit updated statistics"""
344
  socketio.emit('statistics_update', {
345
  'session_id': job.session_id,
346
+ 'statistics': stats,
347
+ 'camera_role': job.camera_role
348
  }, room=job.session_id, namespace='/')
349
 
350
 
 
352
  """Emit processing completion event"""
353
  socketio.emit('processing_complete', {
354
  'session_id': job.session_id,
355
+ 'statistics': final_stats,
356
+ 'camera_role': job.camera_role
357
  }, room=job.session_id, namespace='/')
358
 
359
 
 
361
  """Emit processing error event"""
362
  socketio.emit('processing_error', {
363
  'session_id': job.session_id,
364
+ 'error': error_message,
365
+ 'camera_role': job.camera_role
366
+ }, room=job.session_id, namespace='/')
app/static/js/dashboard.js CHANGED
@@ -8,10 +8,24 @@ class DashboardManager {
8
  this.charts = {};
9
  this.eventCount = 0;
10
  this.maxLogEvents = 50;
11
-
 
 
 
12
  this.init();
13
  }
14
-
 
 
 
 
 
 
 
 
 
 
 
15
  init() {
16
  this.setupCharts();
17
  this.setupSocketIO();
@@ -26,116 +40,112 @@ class DashboardManager {
26
  setupSocketIO() {
27
  // Connect to SocketIO server
28
  this.socket = io();
29
-
30
  this.socket.on('connect', () => {
31
- console.log('Connected to server');
32
- if (this.sessionId) {
33
- this.socket.emit('join_session', { session_id: this.sessionId });
34
- }
35
- });
36
-
37
- this.socket.on('disconnect', () => {
38
- console.log('Disconnected from server');
39
- });
40
-
41
- this.socket.on('connected', (data) => {
42
- console.log('Server acknowledged connection:', data);
43
  });
44
-
45
- // Processing status updates
46
  this.socket.on('processing_status', (data) => {
47
- console.log('Processing status:', data);
48
- this.updateProcessingStatus(data);
49
  });
50
 
51
- // Progress updates
52
  this.socket.on('processing_progress', (data) => {
53
- console.log('Progress:', data.progress + '%');
54
- this.updateProgress(data.progress);
55
  });
56
 
57
- // New vehicle event
58
  this.socket.on('vehicle_event', (data) => {
59
- console.log('New vehicle event:', data.event);
60
  this.addEventToLog(data.event);
61
  });
62
 
63
- // Statistics update
64
  this.socket.on('statistics_update', (data) => {
65
- console.log('Statistics update:', data.statistics);
66
- this.updateStatistics(data.statistics);
67
- this.updateDistributionChart(data.statistics.vehicle_distribution);
 
 
 
68
  });
69
 
70
- // Processing complete
71
  this.socket.on('processing_complete', (data) => {
72
- console.log('Processing complete:', data);
73
  this.onProcessingComplete(data);
 
 
 
 
 
 
 
74
  });
75
 
76
- // Processing error
77
  this.socket.on('processing_error', (data) => {
78
  console.error('Processing error:', data.error);
79
  this.onProcessingError(data.error);
 
 
 
 
 
 
 
80
  });
81
  }
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  async fetchInitialData() {
84
  try {
85
- // Fetch statistics
86
  const statsResponse = await fetch('/api/statistics');
87
  if (statsResponse.ok) {
88
  const stats = await statsResponse.json();
89
- this.updateStatistics(stats);
90
- if (stats.vehicle_distribution) {
91
- this.updateDistributionChart(stats.vehicle_distribution);
92
- }
93
- }
94
-
95
- // Fetch events
96
- const eventsResponse = await fetch('/api/events');
97
- if (eventsResponse.ok) {
98
- const events = await eventsResponse.json();
99
- this.eventCount = events.length;
100
- document.getElementById('event-count').textContent = `${this.eventCount} events`;
101
  }
 
102
  } catch (error) {
103
- console.error('Error fetching initial data:', error);
104
  }
105
  }
106
 
 
107
  updateProcessingStatus(data) {
108
- const banner = document.getElementById('processing-banner');
109
- const text = document.getElementById('processing-text');
110
- const entryStatus = document.getElementById('entry-status');
111
-
112
- if (data.status === 'processing') {
113
- banner.classList.remove('hidden');
114
- text.textContent = `Processing video... ${data.progress}%`;
115
- if (entryStatus) entryStatus.textContent = 'PROCESSING';
116
- } else if (data.status === 'completed') {
117
- banner.classList.add('hidden');
118
- if (entryStatus) entryStatus.textContent = 'COMPLETED';
119
- } else if (data.status === 'error') {
120
- banner.classList.add('hidden');
121
- if (entryStatus) entryStatus.textContent = 'ERROR';
122
- }
123
  }
124
 
125
- updateProgress(progress) {
126
- const progressBar = document.getElementById('progress-bar');
127
- const text = document.getElementById('processing-text');
128
-
129
- if (progressBar) {
130
- progressBar.style.width = `${progress}%`;
131
- }
132
- if (text) {
133
- text.textContent = `Processing video... ${progress}%`;
134
- }
135
- }
136
-
137
- updateStatistics(stats) {
138
- // Update KPI values
139
  const vehiclesIn = document.getElementById('vehicles-in');
140
  const vehiclesOut = document.getElementById('vehicles-out');
141
  const netVehicles = document.getElementById('net-vehicles');
@@ -146,9 +156,11 @@ class DashboardManager {
146
  if (netVehicles) netVehicles.textContent = stats.net_vehicles || 0;
147
 
148
  if (peopleRange) {
149
- const min = stats.people_on_site_min || 0;
150
- const max = stats.people_on_site_max || 0;
151
- peopleRange.textContent = `${min} - ${max}`;
 
 
152
  }
153
  }
154
 
@@ -156,17 +168,14 @@ class DashboardManager {
156
  const tbody = document.getElementById('event-log-body');
157
  if (!tbody) return;
158
 
159
- // Remove "no events" message if present
160
  const emptyRow = tbody.querySelector('.empty-row');
161
  if (emptyRow) {
162
  emptyRow.remove();
163
  }
164
 
165
- // Create new row
166
  const row = document.createElement('tr');
167
  row.className = 'new-event';
168
 
169
- // Format timestamp
170
  let timestamp = '--:--:--';
171
  if (event.timestamp) {
172
  const ts = event.timestamp;
@@ -177,7 +186,6 @@ class DashboardManager {
177
  }
178
  }
179
 
180
- // Direction badge class
181
  const directionClass = event.direction === 'IN' ? 'badge-in' : 'badge-out';
182
 
183
  row.innerHTML = `
@@ -187,19 +195,15 @@ class DashboardManager {
187
  <td>${event.seats_min} - ${event.seats_max}</td>
188
  `;
189
 
190
- // Insert at top
191
  tbody.insertBefore(row, tbody.firstChild);
192
 
193
- // Remove excess rows
194
  while (tbody.children.length > this.maxLogEvents) {
195
  tbody.removeChild(tbody.lastChild);
196
  }
197
 
198
- // Update event count
199
  this.eventCount++;
200
  document.getElementById('event-count').textContent = `${this.eventCount} events`;
201
 
202
- // Highlight animation
203
  setTimeout(() => {
204
  row.classList.remove('new-event');
205
  }, 1000);
@@ -217,16 +221,7 @@ class DashboardManager {
217
  }
218
 
219
  onProcessingComplete(data) {
220
- const banner = document.getElementById('processing-banner');
221
- const entryStatus = document.getElementById('entry-status');
222
-
223
- if (banner) banner.classList.add('hidden');
224
- if (entryStatus) entryStatus.textContent = 'COMPLETED';
225
-
226
- // Show completion notification
227
  this.showNotification('Processing Complete', 'Video analysis finished successfully!', 'success');
228
-
229
- // Update final statistics
230
  if (data.statistics) {
231
  this.updateStatistics(data.statistics);
232
  if (data.statistics.vehicle_distribution) {
@@ -236,17 +231,10 @@ class DashboardManager {
236
  }
237
 
238
  onProcessingError(error) {
239
- const banner = document.getElementById('processing-banner');
240
- const entryStatus = document.getElementById('entry-status');
241
-
242
- if (banner) banner.classList.add('hidden');
243
- if (entryStatus) entryStatus.textContent = 'ERROR';
244
-
245
  this.showNotification('Processing Error', error, 'error');
246
  }
247
 
248
  showNotification(title, message, type = 'info') {
249
- // Create notification element
250
  const notification = document.createElement('div');
251
  notification.className = `notification notification-${type}`;
252
  notification.innerHTML = `
@@ -255,8 +243,6 @@ class DashboardManager {
255
  `;
256
 
257
  document.body.appendChild(notification);
258
-
259
- // Auto-remove after 5 seconds
260
  setTimeout(() => {
261
  notification.classList.add('fade-out');
262
  setTimeout(() => notification.remove(), 300);
@@ -264,7 +250,6 @@ class DashboardManager {
264
  }
265
 
266
  setupCharts() {
267
- // Vehicle Distribution Chart
268
  const distCtx = document.getElementById('distributionChart');
269
  if (distCtx) {
270
  this.charts.distribution = new Chart(distCtx, {
@@ -281,65 +266,10 @@ class DashboardManager {
281
  options: {
282
  responsive: true,
283
  maintainAspectRatio: false,
284
- animation: {
285
- duration: 300
286
- },
287
- plugins: {
288
- legend: { display: false }
289
- },
290
  scales: {
291
- y: {
292
- beginAtZero: true,
293
- grid: { color: '#e2e8f0' }
294
- },
295
- x: {
296
- grid: { display: false }
297
- }
298
- }
299
- }
300
- });
301
- }
302
-
303
- // People Flow Trend Chart
304
- const flowCtx = document.getElementById('flowChart');
305
- if (flowCtx) {
306
- this.charts.flow = new Chart(flowCtx, {
307
- type: 'line',
308
- data: {
309
- labels: [],
310
- datasets: [
311
- {
312
- label: 'Actual Flow',
313
- data: [],
314
- borderColor: '#3b82f6',
315
- backgroundColor: 'rgba(59, 130, 246, 0.2)',
316
- fill: true,
317
- tension: 0.4
318
- },
319
- {
320
- label: 'Predicted Flow',
321
- data: [],
322
- borderColor: '#ef4444',
323
- backgroundColor: 'rgba(239, 68, 68, 0.2)',
324
- fill: true,
325
- tension: 0.4
326
- }
327
- ]
328
- },
329
- options: {
330
- responsive: true,
331
- maintainAspectRatio: false,
332
- plugins: {
333
- legend: { display: false }
334
- },
335
- scales: {
336
- y: {
337
- beginAtZero: true,
338
- grid: { color: '#e2e8f0' }
339
- },
340
- x: {
341
- grid: { display: false }
342
- }
343
  }
344
  }
345
  });
@@ -351,14 +281,11 @@ class DashboardManager {
351
  btn.addEventListener('click', () => {
352
  document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
353
  btn.classList.add('active');
354
- // TODO: Fetch data for selected time period
355
  });
356
  });
357
  }
358
  }
359
 
360
- // Note: Initialization moved to end of file to include WorkbenchManager
361
-
362
  class WorkbenchManager {
363
  constructor() {
364
  // State
@@ -369,8 +296,8 @@ class WorkbenchManager {
369
 
370
  // Track camera configurations separately
371
  this.cameraConfigs = {
372
- 'ENTRY': { hasVideo: false, hasLine: false, videoName: '' },
373
- 'EXIT': { hasVideo: false, hasLine: false, videoName: '' }
374
  };
375
 
376
  // Elements
@@ -388,7 +315,9 @@ class WorkbenchManager {
388
  processingStatus: document.getElementById('processing-status'),
389
  locationInput: document.getElementById('location-input'),
390
  modeLabel: document.getElementById('mode-label'),
391
- liveBadge: document.getElementById('live-badge')
 
 
392
  };
393
 
394
  this.init();
@@ -400,23 +329,17 @@ class WorkbenchManager {
400
  }
401
 
402
  setupEventListeners() {
403
- // Handle Video Upload
404
  if (this.els.videoUpload) {
405
  this.els.videoUpload.addEventListener('change', (e) => this.handleVideoUpload(e));
406
  }
407
-
408
- // Handle Clear/Redraw Line
409
  if (this.els.clearLineBtn) {
410
  this.els.clearLineBtn.addEventListener('click', () => this.resetLine());
411
  }
412
-
413
- // Handle Start Analysis
414
  if (this.els.startBtn) {
415
  this.els.startBtn.addEventListener('click', () => this.startAnalysis());
416
  }
417
  }
418
 
419
- // --- Camera Context Switching (Entry/Exit Tabs) ---
420
  switchCameraContext(role) {
421
  this.currentCameraRole = role;
422
 
@@ -425,11 +348,9 @@ class WorkbenchManager {
425
  const activeId = role === 'ENTRY' ? 'tab-entry' : 'tab-exit';
426
  document.getElementById(activeId).classList.add('active');
427
 
428
- // Update Stage Label
429
  document.getElementById('stage-label').textContent =
430
  `${role.charAt(0).toUpperCase() + role.slice(1).toLowerCase()} Camera Preview`;
431
 
432
- // Update file name display
433
  const config = this.cameraConfigs[role];
434
  if (config.videoName) {
435
  this.els.fileName.textContent = config.videoName;
@@ -437,7 +358,6 @@ class WorkbenchManager {
437
  this.els.fileName.textContent = 'No file selected';
438
  }
439
 
440
- // Load the frame for the selected camera if video exists
441
  if (config.hasVideo) {
442
  this.loadFirstFrame();
443
  this.updateConfigStatus(config.hasLine);
@@ -445,60 +365,101 @@ class WorkbenchManager {
445
  this.els.clearLineBtn.disabled = false;
446
  }
447
  } else {
448
- // No video for this camera - show placeholder
449
  this.els.canvas.style.display = 'none';
450
  this.els.placeholder.style.display = 'block';
451
  this.updateConfigStatus(false);
452
  this.els.clearLineBtn.disabled = true;
453
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  }
455
 
456
- // --- Video Upload & First Frame Extraction ---
457
- async handleVideoUpload(e) {
458
- console.log('handleVideoUpload called', e);
459
 
460
- if (!e.target.files || !e.target.files.length) {
461
- console.log('No files selected');
462
- return;
 
 
 
 
 
463
  }
 
464
 
465
- const file = e.target.files[0];
466
- console.log('File selected:', file.name, 'Size:', file.size, 'Type:', file.type);
467
-
468
- if (this.els.fileName) {
469
- this.els.fileName.textContent = file.name;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  }
 
 
 
 
 
 
471
 
472
  const formData = new FormData();
473
  formData.append('video', file);
474
  formData.append('camera_role', this.currentCameraRole);
475
 
476
  try {
477
- console.log('Uploading to /setup/upload-video for camera:', this.currentCameraRole);
478
-
479
- // Use existing endpoint from setup.py
480
  const response = await fetch('/setup/upload-video', {
481
  method: 'POST',
482
  body: formData
483
  });
484
-
485
- console.log('Response status:', response.status);
486
  const data = await response.json();
487
- console.log('Response data:', data);
488
 
489
  if (data.success) {
490
- // Update camera config
491
  this.cameraConfigs[this.currentCameraRole].hasVideo = true;
492
  this.cameraConfigs[this.currentCameraRole].videoName = file.name;
493
-
494
- console.log('Upload successful, loading first frame...');
495
  this.loadFirstFrame();
496
  } else {
497
- console.error('Upload failed:', data.error);
498
  alert('Upload failed: ' + data.error);
499
  }
500
  } catch (error) {
501
- console.error('Upload error:', error);
502
  alert('Error uploading video: ' + error.message);
503
  }
504
  }
@@ -509,40 +470,53 @@ class WorkbenchManager {
509
  const data = await response.json();
510
 
511
  if (data.frame) {
512
- // UI State: Show Canvas, Hide Placeholder
513
  this.els.placeholder.style.display = 'none';
514
  this.els.canvas.style.display = 'block';
515
  this.els.liveFeed.classList.add('hidden');
516
 
517
- // Initialize Line Drawer
518
  this.lineDrawer = new LineDrawer('setup-canvas');
519
  await this.lineDrawer.loadImage('data:image/jpeg;base64,' + data.frame);
520
 
521
- // If line points exist for this camera, restore them
522
  if (data.line_points) {
523
  this.lineDrawer.setLinePoints(data.line_points);
524
  this.lineSet = true;
525
  this.cameraConfigs[this.currentCameraRole].hasLine = true;
526
  this.els.clearLineBtn.disabled = false;
527
  this.updateConfigStatus(true);
528
- } else {
529
- // Setup Callback for when line is drawn
530
- this.lineDrawer.onLineComplete = (points) => {
531
- this.lineSet = true;
532
- this.cameraConfigs[this.currentCameraRole].hasLine = true;
533
- this.els.clearLineBtn.disabled = false;
534
- this.updateConfigStatus(true);
535
- };
536
-
537
- // Update Status
538
- this.els.configStatusText.textContent = "Video Loaded. Draw Line.";
539
- this.els.configStatusDot.className = "status-dot error"; // Orange/Red until line drawn
540
  }
 
 
 
 
 
 
 
 
 
 
 
541
  }
542
  } catch (error) {
543
  console.error('Error loading frame:', error);
544
  }
545
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
 
547
  resetLine() {
548
  if (this.lineDrawer) {
@@ -565,31 +539,19 @@ class WorkbenchManager {
565
  }
566
  }
567
 
568
- // --- Processing ---
569
  async startAnalysis() {
570
  if (!this.lineSet || !this.lineDrawer) {
571
  alert("Please draw the counting line first.");
572
  return;
573
  }
574
 
575
- // 1. Save Line for current camera
576
- const linePoints = this.lineDrawer.getLinePoints();
577
- await fetch('/setup/save-line', {
578
- method: 'POST',
579
- headers: { 'Content-Type': 'application/json' },
580
- body: JSON.stringify({
581
- line_points: linePoints,
582
- camera_role: this.currentCameraRole
583
- })
584
- });
585
-
586
- // 2. UI Updates (Switch to Analysis Mode)
587
  this.els.processingStatus.classList.remove('hidden');
588
  this.els.startBtn.disabled = true;
589
 
590
  const location = this.els.locationInput.value || 'Unknown';
591
 
592
- // 3. Trigger Processing
593
  try {
594
  const response = await fetch('/setup/start-processing', {
595
  method: 'POST',
@@ -602,10 +564,13 @@ class WorkbenchManager {
602
  const data = await response.json();
603
 
604
  if (data.success) {
605
- // Switch View to "Analysis Mode"
 
 
 
 
606
  this.setAnalysisMode(true);
607
 
608
- // Join the session room for real-time updates
609
  if (window.dashboardManager && window.dashboardManager.socket) {
610
  window.dashboardManager.sessionId = data.session_id;
611
  window.dashboardManager.socket.emit('join_session', {
@@ -622,20 +587,15 @@ class WorkbenchManager {
622
 
623
  setAnalysisMode(isActive) {
624
  if (isActive) {
625
- // Hide Setup Canvas, Show Live Feed (which will be populated by processed video)
626
  this.els.canvas.style.display = 'none';
627
  this.els.liveFeed.classList.remove('hidden');
628
  this.els.liveBadge.classList.remove('hidden');
629
  this.els.modeLabel.textContent = "Live Analysis";
630
-
631
- // In a real app, you'd set the src of liveFeed to the processed stream URL
632
- // For now, we rely on the dashboard.js socket updates to handle data
633
  }
634
  }
635
 
636
  checkExistingSession() {
637
- // If the page loads and we already have a session, switch to analysis mode
638
- // (This relies on backend session logic or a global variable)
639
  const hasSession = document.getElementById('live-badge').classList.contains('active-session');
640
  if(hasSession) {
641
  this.setAnalysisMode(true);
@@ -643,18 +603,13 @@ class WorkbenchManager {
643
  }
644
  }
645
 
646
- // Global function for the HTML onclick attributes
647
  function switchCameraContext(role) {
648
  if (window.workbenchManager) {
649
  window.workbenchManager.switchCameraContext(role);
650
  }
651
  }
652
 
653
- // Initialize when DOM is ready
654
  document.addEventListener('DOMContentLoaded', () => {
655
- // Initialize Dashboard (Analytics)
656
  window.dashboardManager = new DashboardManager();
657
-
658
- // Initialize Workbench (Setup/UI)
659
  window.workbenchManager = new WorkbenchManager();
660
- });
 
8
  this.charts = {};
9
  this.eventCount = 0;
10
  this.maxLogEvents = 50;
11
+ this.cameraStats = {
12
+ 'ENTRY': this.createEmptyStats(),
13
+ 'EXIT': this.createEmptyStats()
14
+ };
15
  this.init();
16
  }
17
+
18
+ createEmptyStats() {
19
+ return {
20
+ vehicles_in: 0,
21
+ vehicles_out: 0,
22
+ net_vehicles: 0,
23
+ people_on_site_min: 0,
24
+ people_on_site_max: 0,
25
+ vehicle_distribution: {}
26
+ };
27
+ }
28
+
29
  init() {
30
  this.setupCharts();
31
  this.setupSocketIO();
 
40
  setupSocketIO() {
41
  // Connect to SocketIO server
42
  this.socket = io();
 
43
  this.socket.on('connect', () => {
44
+ if (this.sessionId) this.socket.emit('join_session', { session_id: this.sessionId });
 
 
 
 
 
 
 
 
 
 
 
45
  });
46
+
 
47
  this.socket.on('processing_status', (data) => {
48
+ if (window.workbenchManager) window.workbenchManager.handleProcessingUpdate(data);
 
49
  });
50
 
 
51
  this.socket.on('processing_progress', (data) => {
52
+ if (window.workbenchManager) window.workbenchManager.handleProgressUpdate(data);
 
53
  });
54
 
 
55
  this.socket.on('vehicle_event', (data) => {
 
56
  this.addEventToLog(data.event);
57
  });
58
 
59
+ // CHANGED: Handle statistics per camera
60
  this.socket.on('statistics_update', (data) => {
61
+ if (data.camera_role) {
62
+ this.updateCameraStatistics(data.camera_role, data.statistics);
63
+ } else {
64
+ // Fallback for initial load or legacy events
65
+ this.updateAggregatedStatistics(data.statistics);
66
+ }
67
  });
68
 
 
69
  this.socket.on('processing_complete', (data) => {
 
70
  this.onProcessingComplete(data);
71
+ if (window.workbenchManager) {
72
+ window.workbenchManager.handleProcessingUpdate({
73
+ camera_role: data.camera_role,
74
+ status: 'completed',
75
+ progress: 100
76
+ });
77
+ }
78
  });
79
 
 
80
  this.socket.on('processing_error', (data) => {
81
  console.error('Processing error:', data.error);
82
  this.onProcessingError(data.error);
83
+ if (window.workbenchManager) {
84
+ window.workbenchManager.handleProcessingUpdate({
85
+ camera_role: data.camera_role,
86
+ status: 'error',
87
+ progress: 0
88
+ });
89
+ }
90
  });
91
  }
92
 
93
+ // Update stats for a specific camera and then refresh display
94
+ updateCameraStatistics(role, stats) {
95
+ this.cameraStats[role] = stats;
96
+ this.refreshDashboard();
97
+ }
98
+
99
+ // Calculate totals and update UI
100
+ refreshDashboard() {
101
+ const entry = this.cameraStats.ENTRY;
102
+ const exit = this.cameraStats.EXIT;
103
+
104
+ // Sum distribution
105
+ const totalDist = {};
106
+ const allTypes = new Set([
107
+ ...Object.keys(entry.vehicle_distribution || {}),
108
+ ...Object.keys(exit.vehicle_distribution || {})
109
+ ]);
110
+
111
+ allTypes.forEach(type => {
112
+ totalDist[type] = (entry.vehicle_distribution[type] || 0) +
113
+ (exit.vehicle_distribution[type] || 0);
114
+ });
115
+
116
+ const aggregated = {
117
+ vehicles_in: (entry.vehicles_in || 0) + (exit.vehicles_in || 0),
118
+ vehicles_out: (entry.vehicles_out || 0) + (exit.vehicles_out || 0),
119
+ net_vehicles: (entry.net_vehicles || 0) + (exit.net_vehicles || 0),
120
+ people_on_site_min: (entry.people_on_site_min || 0) + (exit.people_on_site_min || 0),
121
+ people_on_site_max: (entry.people_on_site_max || 0) + (exit.people_on_site_max || 0),
122
+ vehicle_distribution: totalDist
123
+ };
124
+
125
+ this.updateAggregatedStatistics(aggregated);
126
+ }
127
+
128
  async fetchInitialData() {
129
  try {
 
130
  const statsResponse = await fetch('/api/statistics');
131
  if (statsResponse.ok) {
132
  const stats = await statsResponse.json();
133
+ // Initial load is likely the total from DB, so we display it directly
134
+ // Note: Real-time updates will takeover shortly
135
+ this.updateAggregatedStatistics(stats);
 
 
 
 
 
 
 
 
 
136
  }
137
+ // ... events fetch ...
138
  } catch (error) {
139
+ console.error(error);
140
  }
141
  }
142
 
143
+ // Legacy support or global banner
144
  updateProcessingStatus(data) {
145
+ // Implementation moved to WorkbenchManager for specific handling
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  }
147
 
148
+ updateAggregatedStatistics(stats) {
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  const vehiclesIn = document.getElementById('vehicles-in');
150
  const vehiclesOut = document.getElementById('vehicles-out');
151
  const netVehicles = document.getElementById('net-vehicles');
 
156
  if (netVehicles) netVehicles.textContent = stats.net_vehicles || 0;
157
 
158
  if (peopleRange) {
159
+ peopleRange.textContent = `${stats.people_on_site_min} - ${stats.people_on_site_max}`;
160
+ }
161
+
162
+ if (stats.vehicle_distribution) {
163
+ this.updateDistributionChart(stats.vehicle_distribution);
164
  }
165
  }
166
 
 
168
  const tbody = document.getElementById('event-log-body');
169
  if (!tbody) return;
170
 
 
171
  const emptyRow = tbody.querySelector('.empty-row');
172
  if (emptyRow) {
173
  emptyRow.remove();
174
  }
175
 
 
176
  const row = document.createElement('tr');
177
  row.className = 'new-event';
178
 
 
179
  let timestamp = '--:--:--';
180
  if (event.timestamp) {
181
  const ts = event.timestamp;
 
186
  }
187
  }
188
 
 
189
  const directionClass = event.direction === 'IN' ? 'badge-in' : 'badge-out';
190
 
191
  row.innerHTML = `
 
195
  <td>${event.seats_min} - ${event.seats_max}</td>
196
  `;
197
 
 
198
  tbody.insertBefore(row, tbody.firstChild);
199
 
 
200
  while (tbody.children.length > this.maxLogEvents) {
201
  tbody.removeChild(tbody.lastChild);
202
  }
203
 
 
204
  this.eventCount++;
205
  document.getElementById('event-count').textContent = `${this.eventCount} events`;
206
 
 
207
  setTimeout(() => {
208
  row.classList.remove('new-event');
209
  }, 1000);
 
221
  }
222
 
223
  onProcessingComplete(data) {
 
 
 
 
 
 
 
224
  this.showNotification('Processing Complete', 'Video analysis finished successfully!', 'success');
 
 
225
  if (data.statistics) {
226
  this.updateStatistics(data.statistics);
227
  if (data.statistics.vehicle_distribution) {
 
231
  }
232
 
233
  onProcessingError(error) {
 
 
 
 
 
 
234
  this.showNotification('Processing Error', error, 'error');
235
  }
236
 
237
  showNotification(title, message, type = 'info') {
 
238
  const notification = document.createElement('div');
239
  notification.className = `notification notification-${type}`;
240
  notification.innerHTML = `
 
243
  `;
244
 
245
  document.body.appendChild(notification);
 
 
246
  setTimeout(() => {
247
  notification.classList.add('fade-out');
248
  setTimeout(() => notification.remove(), 300);
 
250
  }
251
 
252
  setupCharts() {
 
253
  const distCtx = document.getElementById('distributionChart');
254
  if (distCtx) {
255
  this.charts.distribution = new Chart(distCtx, {
 
266
  options: {
267
  responsive: true,
268
  maintainAspectRatio: false,
269
+ plugins: { legend: { display: false } },
 
 
 
 
 
270
  scales: {
271
+ y: { beginAtZero: true, grid: { color: '#e2e8f0' } },
272
+ x: { grid: { display: false } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  }
274
  }
275
  });
 
281
  btn.addEventListener('click', () => {
282
  document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
283
  btn.classList.add('active');
 
284
  });
285
  });
286
  }
287
  }
288
 
 
 
289
  class WorkbenchManager {
290
  constructor() {
291
  // State
 
296
 
297
  // Track camera configurations separately
298
  this.cameraConfigs = {
299
+ 'ENTRY': { hasVideo: false, hasLine: false, videoName: '', processingStatus: 'pending', progress: 0 },
300
+ 'EXIT': { hasVideo: false, hasLine: false, videoName: '', processingStatus: 'pending', progress: 0 }
301
  };
302
 
303
  // Elements
 
315
  processingStatus: document.getElementById('processing-status'),
316
  locationInput: document.getElementById('location-input'),
317
  modeLabel: document.getElementById('mode-label'),
318
+ liveBadge: document.getElementById('live-badge'),
319
+ progressBar: document.getElementById('progress-bar'),
320
+ processingText: document.getElementById('processing-text')
321
  };
322
 
323
  this.init();
 
329
  }
330
 
331
  setupEventListeners() {
 
332
  if (this.els.videoUpload) {
333
  this.els.videoUpload.addEventListener('change', (e) => this.handleVideoUpload(e));
334
  }
 
 
335
  if (this.els.clearLineBtn) {
336
  this.els.clearLineBtn.addEventListener('click', () => this.resetLine());
337
  }
 
 
338
  if (this.els.startBtn) {
339
  this.els.startBtn.addEventListener('click', () => this.startAnalysis());
340
  }
341
  }
342
 
 
343
  switchCameraContext(role) {
344
  this.currentCameraRole = role;
345
 
 
348
  const activeId = role === 'ENTRY' ? 'tab-entry' : 'tab-exit';
349
  document.getElementById(activeId).classList.add('active');
350
 
 
351
  document.getElementById('stage-label').textContent =
352
  `${role.charAt(0).toUpperCase() + role.slice(1).toLowerCase()} Camera Preview`;
353
 
 
354
  const config = this.cameraConfigs[role];
355
  if (config.videoName) {
356
  this.els.fileName.textContent = config.videoName;
 
358
  this.els.fileName.textContent = 'No file selected';
359
  }
360
 
 
361
  if (config.hasVideo) {
362
  this.loadFirstFrame();
363
  this.updateConfigStatus(config.hasLine);
 
365
  this.els.clearLineBtn.disabled = false;
366
  }
367
  } else {
 
368
  this.els.canvas.style.display = 'none';
369
  this.els.placeholder.style.display = 'block';
370
  this.updateConfigStatus(false);
371
  this.els.clearLineBtn.disabled = true;
372
  }
373
+
374
+ // Update action panel UI to reflect status of THIS camera
375
+ this.updateActionPanelUI();
376
+ }
377
+
378
+ // NEW: Handle processing updates from socket
379
+ handleProcessingUpdate(data) {
380
+ if (!data.camera_role) return;
381
+
382
+ const config = this.cameraConfigs[data.camera_role];
383
+ if (config) {
384
+ config.processingStatus = data.status;
385
+ if (data.progress) config.progress = data.progress;
386
+
387
+ if (this.currentCameraRole === data.camera_role) {
388
+ this.updateActionPanelUI();
389
+ }
390
+ }
391
  }
392
 
393
+ handleProgressUpdate(data) {
394
+ if (!data.camera_role) return;
 
395
 
396
+ const config = this.cameraConfigs[data.camera_role];
397
+ if (config) {
398
+ config.progress = data.progress;
399
+ if (config.processingStatus === 'pending') config.processingStatus = 'processing';
400
+
401
+ if (this.currentCameraRole === data.camera_role) {
402
+ this.updateActionPanelUI();
403
+ }
404
  }
405
+ }
406
 
407
+ updateActionPanelUI() {
408
+ const config = this.cameraConfigs[this.currentCameraRole];
409
+ const status = config.processingStatus;
410
+
411
+ if (status === 'processing') {
412
+ this.els.processingStatus.classList.remove('hidden');
413
+ this.els.startBtn.disabled = true;
414
+ this.els.startBtn.innerHTML = `<i data-feather="loader" class="spin"></i> Processing...`;
415
+
416
+ if (this.els.progressBar) {
417
+ this.els.progressBar.style.width = (config.progress || 0) + '%';
418
+ }
419
+ if (this.els.processingText) {
420
+ this.els.processingText.textContent = `Processing... ${config.progress || 0}%`;
421
+ }
422
+ feather.replace();
423
+ } else if (status === 'completed') {
424
+ this.els.processingStatus.classList.add('hidden');
425
+ this.els.startBtn.disabled = true;
426
+ this.els.startBtn.innerHTML = `<i data-feather="check"></i> Completed`;
427
+ this.els.configStatusText.textContent = "Analysis Complete";
428
+ this.els.configStatusDot.className = "status-dot ready";
429
+ feather.replace();
430
+ } else {
431
+ // Pending or reset
432
+ this.els.processingStatus.classList.add('hidden');
433
+ this.els.startBtn.innerHTML = `<i data-feather="play"></i> Start Analysis`;
434
+ this.updateConfigStatus(config.hasLine); // Re-enable if ready
435
+ feather.replace();
436
  }
437
+ }
438
+
439
+ async handleVideoUpload(e) {
440
+ if (!e.target.files || !e.target.files.length) return;
441
+ const file = e.target.files[0];
442
+ this.els.fileName.textContent = file.name;
443
 
444
  const formData = new FormData();
445
  formData.append('video', file);
446
  formData.append('camera_role', this.currentCameraRole);
447
 
448
  try {
 
 
 
449
  const response = await fetch('/setup/upload-video', {
450
  method: 'POST',
451
  body: formData
452
  });
 
 
453
  const data = await response.json();
 
454
 
455
  if (data.success) {
 
456
  this.cameraConfigs[this.currentCameraRole].hasVideo = true;
457
  this.cameraConfigs[this.currentCameraRole].videoName = file.name;
 
 
458
  this.loadFirstFrame();
459
  } else {
 
460
  alert('Upload failed: ' + data.error);
461
  }
462
  } catch (error) {
 
463
  alert('Error uploading video: ' + error.message);
464
  }
465
  }
 
470
  const data = await response.json();
471
 
472
  if (data.frame) {
 
473
  this.els.placeholder.style.display = 'none';
474
  this.els.canvas.style.display = 'block';
475
  this.els.liveFeed.classList.add('hidden');
476
 
 
477
  this.lineDrawer = new LineDrawer('setup-canvas');
478
  await this.lineDrawer.loadImage('data:image/jpeg;base64,' + data.frame);
479
 
 
480
  if (data.line_points) {
481
  this.lineDrawer.setLinePoints(data.line_points);
482
  this.lineSet = true;
483
  this.cameraConfigs[this.currentCameraRole].hasLine = true;
484
  this.els.clearLineBtn.disabled = false;
485
  this.updateConfigStatus(true);
 
 
 
 
 
 
 
 
 
 
 
 
486
  }
487
+
488
+ // NEW: Attach Callback with Auto-Save
489
+ this.lineDrawer.onLineComplete = (points) => {
490
+ this.lineSet = true;
491
+ this.cameraConfigs[this.currentCameraRole].hasLine = true;
492
+ this.els.clearLineBtn.disabled = false;
493
+ this.updateConfigStatus(true);
494
+
495
+ // FIX: Automatically save line to backend
496
+ this.saveLine(points);
497
+ };
498
  }
499
  } catch (error) {
500
  console.error('Error loading frame:', error);
501
  }
502
  }
503
+
504
+ // Helper to auto-save
505
+ async saveLine(points) {
506
+ try {
507
+ await fetch('/setup/save-line', {
508
+ method: 'POST',
509
+ headers: { 'Content-Type': 'application/json' },
510
+ body: JSON.stringify({
511
+ line_points: points,
512
+ camera_role: this.currentCameraRole
513
+ })
514
+ });
515
+ console.log('Line auto-saved');
516
+ } catch (e) {
517
+ console.error('Failed to auto-save line', e);
518
+ }
519
+ }
520
 
521
  resetLine() {
522
  if (this.lineDrawer) {
 
539
  }
540
  }
541
 
 
542
  async startAnalysis() {
543
  if (!this.lineSet || !this.lineDrawer) {
544
  alert("Please draw the counting line first.");
545
  return;
546
  }
547
 
548
+ // Line is already auto-saved, but we can double check or just proceed
549
+
 
 
 
 
 
 
 
 
 
 
550
  this.els.processingStatus.classList.remove('hidden');
551
  this.els.startBtn.disabled = true;
552
 
553
  const location = this.els.locationInput.value || 'Unknown';
554
 
 
555
  try {
556
  const response = await fetch('/setup/start-processing', {
557
  method: 'POST',
 
564
  const data = await response.json();
565
 
566
  if (data.success) {
567
+ // Update local state
568
+ const config = this.cameraConfigs[this.currentCameraRole];
569
+ config.processingStatus = 'processing';
570
+ this.updateActionPanelUI();
571
+
572
  this.setAnalysisMode(true);
573
 
 
574
  if (window.dashboardManager && window.dashboardManager.socket) {
575
  window.dashboardManager.sessionId = data.session_id;
576
  window.dashboardManager.socket.emit('join_session', {
 
587
 
588
  setAnalysisMode(isActive) {
589
  if (isActive) {
 
590
  this.els.canvas.style.display = 'none';
591
  this.els.liveFeed.classList.remove('hidden');
592
  this.els.liveBadge.classList.remove('hidden');
593
  this.els.modeLabel.textContent = "Live Analysis";
 
 
 
594
  }
595
  }
596
 
597
  checkExistingSession() {
598
+ // Simple check
 
599
  const hasSession = document.getElementById('live-badge').classList.contains('active-session');
600
  if(hasSession) {
601
  this.setAnalysisMode(true);
 
603
  }
604
  }
605
 
 
606
  function switchCameraContext(role) {
607
  if (window.workbenchManager) {
608
  window.workbenchManager.switchCameraContext(role);
609
  }
610
  }
611
 
 
612
  document.addEventListener('DOMContentLoaded', () => {
 
613
  window.dashboardManager = new DashboardManager();
 
 
614
  window.workbenchManager = new WorkbenchManager();
615
+ });
app/templates/setup.html DELETED
@@ -1,335 +0,0 @@
1
- {% extends "base.html" %}
2
-
3
- {% block title %}Configuration - R&R Vehicle Analytics{% endblock %}
4
-
5
- {% block content %}
6
- <div class="setup-layout">
7
- <!-- Left Sidebar -->
8
- <aside class="setup-sidebar">
9
- <a href="{{ url_for('dashboard.index') }}" class="back-link">
10
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
11
- <path d="M19 12H5M12 19l-7-7 7-7"/>
12
- </svg>
13
- Back
14
- </a>
15
-
16
- <!-- Input Source Section -->
17
- <div class="sidebar-section">
18
- <h3 class="sidebar-title">
19
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
20
- <circle cx="12" cy="12" r="10"/>
21
- <circle cx="12" cy="12" r="3"/>
22
- </svg>
23
- Input Source
24
- </h3>
25
-
26
- <div class="form-group">
27
- <label class="form-label">Camera Role</label>
28
- <select class="form-select" id="camera-role">
29
- <option value="ENTRY">Entry Camera</option>
30
- <option value="EXIT">Exit Camera</option>
31
- </select>
32
- </div>
33
-
34
- <div class="form-group">
35
- <label class="form-label">Select Source Type</label>
36
- <select class="form-select" id="source-type">
37
- <option value="">Select</option>
38
- <option value="video">Video File</option>
39
- <option value="camera">Live Camera</option>
40
- </select>
41
- </div>
42
-
43
- <div id="video-upload-section">
44
- <input type="file" id="video-input" accept="video/*" class="hidden">
45
- <button class="btn btn-primary btn-full" id="browse-btn">
46
- Browse Files
47
- </button>
48
- <div id="file-name" class="file-name-display"></div>
49
- </div>
50
-
51
- <div id="camera-section" class="hidden">
52
- <div class="form-group">
53
- <label class="form-label">Camera URL</label>
54
- <input type="text" class="form-input" id="camera-url" placeholder="rtsp://...">
55
- </div>
56
- </div>
57
- </div>
58
-
59
- <!-- Location Section -->
60
- <div class="sidebar-section">
61
- <div class="form-group">
62
- <label class="form-label">R&R Location</label>
63
- <input type="text" class="form-input" id="location-input" placeholder="e.g., Skudai">
64
- </div>
65
- </div>
66
-
67
- <!-- Action Panel -->
68
- <div class="sidebar-section">
69
- <h3 class="sidebar-title">
70
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
71
- <path d="M12 3v18M3 12h18"/>
72
- </svg>
73
- Action Panel
74
- </h3>
75
-
76
- <!-- <button class="btn btn-primary btn-full" id="set-line-btn" disabled>
77
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
78
- <path d="M12 19l7-7 3 3-7 7-3-3z"/>
79
- <path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/>
80
- </svg>
81
- Set Counting Line
82
- </button> -->
83
-
84
- <button class="btn btn-outline btn-full" id="reset-line-btn" disabled>
85
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
86
- <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
87
- <path d="M3 3v5h5"/>
88
- </svg>
89
- Reset Line
90
- </button>
91
-
92
- <hr class="sidebar-divider">
93
-
94
- <button class="btn btn-success btn-full" id="save-config-btn" disabled>
95
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
96
- <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
97
- <polyline points="17,21 17,13 7,13 7,21"/>
98
- <polyline points="7,3 7,8 15,8"/>
99
- </svg>
100
- Start Processing
101
- </button>
102
- </div>
103
- </aside>
104
-
105
- <!-- Main Content -->
106
- <main class="setup-main">
107
- <!-- Instruction Alert -->
108
- <div class="alert alert-warning">
109
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
110
- <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
111
- <line x1="12" y1="9" x2="12" y2="13"/>
112
- <line x1="12" y1="17" x2="12.01" y2="17"/>
113
- </svg>
114
- <div>
115
- <strong>Instruction:</strong>
116
- Drag the endpoints of the line to define the entry/exit threshold.
117
- The line should be placed perpendicular to vehicle traffic flow.
118
- </div>
119
- </div>
120
-
121
- <!-- Video Preview -->
122
- <div class="preview-section">
123
- <h3 class="preview-title">Video Preview</h3>
124
-
125
- <div class="canvas-wrapper">
126
- <div class="canvas-container" id="canvas-container">
127
- <canvas id="preview-canvas"></canvas>
128
- <div class="canvas-placeholder" id="canvas-placeholder">
129
- <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
130
- <path d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
131
- </svg>
132
- <p>Video Feed Preview</p>
133
- <span class="canvas-info" id="frame-info">Frame: 1/1200 | Time: 00:00:00</span>
134
- </div>
135
- </div>
136
- </div>
137
-
138
- <div class="line-position">
139
- <span>Line Position:</span>
140
- <span class="position-value" id="line-position">X: --, Y: --</span>
141
- </div>
142
- </div>
143
-
144
- <!-- Processing Progress (hidden by default) -->
145
- <div class="processing-section hidden" id="processing-section">
146
- <h3>Processing Video...</h3>
147
- <div class="progress-bar">
148
- <div class="progress-bar-fill" id="progress-fill" style="width: 0%"></div>
149
- </div>
150
- <p class="progress-text" id="progress-text">0% complete</p>
151
- </div>
152
- </main>
153
- </div>
154
- {% endblock %}
155
-
156
- {% block scripts %}
157
- <script src="{{ url_for('static', filename='js/setup_canvas.js') }}"></script>
158
- <script>
159
- document.addEventListener('DOMContentLoaded', function() {
160
- const sourceType = document.getElementById('source-type');
161
- const videoInput = document.getElementById('video-input');
162
- const browseBtn = document.getElementById('browse-btn');
163
- const fileNameDisplay = document.getElementById('file-name');
164
- const videoUploadSection = document.getElementById('video-upload-section');
165
- const cameraSection = document.getElementById('camera-section');
166
- // const setLineBtn = document.getElementById('set-line-btn');
167
- const resetLineBtn = document.getElementById('reset-line-btn');
168
- const saveConfigBtn = document.getElementById('save-config-btn');
169
- const canvasPlaceholder = document.getElementById('canvas-placeholder');
170
- const linePosition = document.getElementById('line-position');
171
- const processingSection = document.getElementById('processing-section');
172
-
173
- let lineDrawer = null;
174
- let lineSet = false;
175
-
176
- // Source type change
177
- sourceType.addEventListener('change', function() {
178
- if (this.value === 'video') {
179
- videoUploadSection.classList.remove('hidden');
180
- cameraSection.classList.add('hidden');
181
- } else if (this.value === 'camera') {
182
- videoUploadSection.classList.add('hidden');
183
- cameraSection.classList.remove('hidden');
184
- }
185
- });
186
-
187
- // Browse button click
188
- browseBtn.addEventListener('click', function() {
189
- videoInput.click();
190
- });
191
-
192
- // Video file selected
193
- videoInput.addEventListener('change', async function() {
194
- if (this.files.length > 0) {
195
- const file = this.files[0];
196
- fileNameDisplay.textContent = file.name;
197
-
198
- // Upload video
199
- const formData = new FormData();
200
- formData.append('video', file);
201
-
202
- try {
203
- const response = await fetch('/setup/upload-video', {
204
- method: 'POST',
205
- body: formData
206
- });
207
-
208
- const data = await response.json();
209
-
210
- if (data.success) {
211
- // Get first frame
212
- await loadFirstFrame();
213
- // setLineBtn.disabled = false;
214
- } else {
215
- alert('Error uploading video: ' + data.error);
216
- }
217
- } catch (error) {
218
- console.error('Upload error:', error);
219
- alert('Failed to upload video');
220
- }
221
- }
222
- });
223
-
224
- async function loadFirstFrame() {
225
- try {
226
- const response = await fetch('/setup/get-first-frame');
227
- const data = await response.json();
228
-
229
- if (data.frame) {
230
- // Hide placeholder first
231
- canvasPlaceholder.style.display = 'none';
232
- canvasPlaceholder.classList.add('hidden');
233
-
234
- // Initialize line drawer
235
- lineDrawer = new LineDrawer('preview-canvas');
236
- await lineDrawer.loadImage('data:image/jpeg;base64,' + data.frame);
237
-
238
- // Update instruction
239
- console.log('Frame loaded, canvas ready for drawing');
240
-
241
- // Line complete callback
242
- lineDrawer.onLineComplete = function(points) {
243
- lineSet = true;
244
- resetLineBtn.disabled = false;
245
- saveConfigBtn.disabled = false;
246
-
247
- const startX = Math.round(points[0][0]);
248
- const startY = Math.round(points[0][1]);
249
- const endX = Math.round(points[1][0]);
250
- const endY = Math.round(points[1][1]);
251
-
252
- linePosition.textContent = `Start: (${startX}, ${startY}) → End: (${endX}, ${endY})`;
253
- };
254
- } else {
255
- console.error('No frame data received');
256
- alert('Failed to load video frame');
257
- }
258
- } catch (error) {
259
- console.error('Error loading frame:', error);
260
- alert('Error loading video frame: ' + error.message);
261
- }
262
- }
263
-
264
- // Set counting line button - just provides instruction
265
- // setLineBtn.addEventListener('click', function() {
266
- // if (lineDrawer) {
267
- // alert('Click and drag on the video preview to draw the counting line');
268
- // } else {
269
- // alert('Please upload a video first');
270
- // }
271
- // });
272
-
273
- // Reset line button
274
- resetLineBtn.addEventListener('click', function() {
275
- if (lineDrawer) {
276
- lineDrawer.reset();
277
- lineSet = false;
278
- saveConfigBtn.disabled = true;
279
- linePosition.textContent = 'X: --, Y: --';
280
- }
281
- });
282
-
283
- // Save configuration / Start processing
284
- saveConfigBtn.addEventListener('click', async function() {
285
- if (!lineDrawer || !lineSet) {
286
- alert('Please draw a counting line first');
287
- return;
288
- }
289
-
290
- const linePoints = lineDrawer.getLinePoints();
291
- const location = document.getElementById('location-input').value || 'Unknown';
292
- const cameraRole = document.getElementById('camera-role').value;
293
-
294
- try {
295
- // Save line coordinates
296
- await fetch('/setup/save-line', {
297
- method: 'POST',
298
- headers: { 'Content-Type': 'application/json' },
299
- body: JSON.stringify({ line_points: linePoints })
300
- });
301
-
302
- // Show processing section
303
- processingSection.classList.remove('hidden');
304
- saveConfigBtn.disabled = true;
305
-
306
- // Start processing
307
- const response = await fetch('/setup/start-processing', {
308
- method: 'POST',
309
- headers: { 'Content-Type': 'application/json' },
310
- body: JSON.stringify({
311
- location: location,
312
- camera_role: cameraRole
313
- })
314
- });
315
-
316
- const data = await response.json();
317
-
318
- if (data.success) {
319
- // Redirect to dashboard
320
- window.location.href = '/';
321
- } else {
322
- alert('Processing error: ' + data.error);
323
- processingSection.classList.add('hidden');
324
- saveConfigBtn.disabled = false;
325
- }
326
- } catch (error) {
327
- console.error('Error:', error);
328
- alert('Failed to start processing');
329
- processingSection.classList.add('hidden');
330
- saveConfigBtn.disabled = false;
331
- }
332
- });
333
- });
334
- </script>
335
- {% endblock %}