Clean push: Removed heavy files & added only latest snapshot

This commit is contained in:
2025-07-26 05:16:12 +05:30
commit acf84e8767
250 changed files with 58564 additions and 0 deletions

View File

@@ -0,0 +1,476 @@
"""
Analytics View - Traffic analytics and reporting
Displays charts, statistics, and historical data.
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QGroupBox, QGridLayout, QFrame, QScrollArea, QTabWidget,
QTableWidget, QTableWidgetItem, QHeaderView, QDateEdit,
QComboBox, QSpinBox
)
from PySide6.QtCore import Qt, Signal, Slot, QTimer, QDate
from PySide6.QtGui import QPixmap, QPainter, QBrush, QColor, QFont
from datetime import datetime, timedelta
import json
# Import finale components
try:
# Try relative imports first (when running as a package)
from ..styles import FinaleStyles, MaterialColors
from ..icons import FinaleIcons
# Import advanced chart components from original analytics_tab
import sys
import os
from pathlib import Path
# Add parent directory to path to import from qt_app_pyside
sys.path.append(str(Path(__file__).parent.parent.parent))
from qt_app_pyside.ui.analytics_tab import ChartWidget, TimeSeriesChart, DetectionPieChart, ViolationBarChart
from qt_app_pyside.controllers.analytics_controller import AnalyticsController
from qt_app_pyside.utils.helpers import load_configuration, format_timestamp, format_duration
except ImportError:
# Fallback for direct execution
try:
from styles import FinaleStyles, MaterialColors
from icons import FinaleIcons
# Create simplified chart widgets if advanced ones not available
except ImportError:
print("Error importing analytics components")
class ChartWidget(QWidget):
def __init__(self, title="Chart"):
super().__init__()
self.title = title
self.data = []
self.chart_type = "line" # line, bar, pie
self.setMinimumSize(400, 300)
def set_data(self, data, chart_type="line"):
"""Set chart data and type"""
self.data = data
self.chart_type = chart_type
self.update()
def paintEvent(self, event):
"""Paint the chart"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Background
painter.fillRect(self.rect(), QColor(MaterialColors.surface))
# Border
painter.setPen(QColor(MaterialColors.outline))
painter.drawRect(self.rect().adjusted(0, 0, -1, -1))
# Title
painter.setPen(QColor(MaterialColors.text_primary))
painter.setFont(QFont("Segoe UI", 12, QFont.Bold))
title_rect = self.rect().adjusted(10, 10, -10, -10)
painter.drawText(title_rect, Qt.AlignTop | Qt.AlignLeft, self.title)
# Chart area
chart_rect = self.rect().adjusted(50, 50, -20, -50)
if not self.data:
# No data message
painter.setPen(QColor(MaterialColors.text_secondary))
painter.setFont(QFont("Segoe UI", 10))
painter.drawText(chart_rect, Qt.AlignCenter, "No data available")
return
# Draw chart based on type
if self.chart_type == "line":
self.draw_line_chart(painter, chart_rect)
elif self.chart_type == "bar":
self.draw_bar_chart(painter, chart_rect)
elif self.chart_type == "pie":
self.draw_pie_chart(painter, chart_rect)
def draw_line_chart(self, painter, rect):
"""Draw a line chart"""
if len(self.data) < 2:
return
# Find min/max values
values = [item.get('value', 0) for item in self.data]
min_val, max_val = min(values), max(values)
if max_val == min_val:
max_val = min_val + 1
# Calculate points
points = []
for i, item in enumerate(self.data):
x = rect.left() + (i / (len(self.data) - 1)) * rect.width()
y = rect.bottom() - ((item.get('value', 0) - min_val) / (max_val - min_val)) * rect.height()
points.append((x, y))
# Draw grid lines
painter.setPen(QColor(MaterialColors.outline_variant))
for i in range(5):
y = rect.top() + (i / 4) * rect.height()
painter.drawLine(rect.left(), y, rect.right(), y)
# Draw line
painter.setPen(QColor(MaterialColors.primary))
for i in range(len(points) - 1):
painter.drawLine(points[i][0], points[i][1], points[i+1][0], points[i+1][1])
# Draw points
painter.setBrush(QBrush(QColor(MaterialColors.primary)))
for x, y in points:
painter.drawEllipse(x-3, y-3, 6, 6)
def draw_bar_chart(self, painter, rect):
"""Draw a bar chart"""
if not self.data:
return
values = [item.get('value', 0) for item in self.data]
max_val = max(values) if values else 1
bar_width = rect.width() / len(self.data) * 0.8
spacing = rect.width() / len(self.data) * 0.2
painter.setBrush(QBrush(QColor(MaterialColors.primary)))
for i, item in enumerate(self.data):
value = item.get('value', 0)
height = (value / max_val) * rect.height()
x = rect.left() + i * (bar_width + spacing) + spacing / 2
y = rect.bottom() - height
painter.drawRect(x, y, bar_width, height)
def draw_pie_chart(self, painter, rect):
"""Draw a pie chart"""
if not self.data:
return
total = sum(item.get('value', 0) for item in self.data)
if total == 0:
return
# Calculate center and radius
center = rect.center()
radius = min(rect.width(), rect.height()) // 2 - 20
# Colors for pie slices
colors = [MaterialColors.primary, MaterialColors.secondary, MaterialColors.tertiary,
MaterialColors.error, MaterialColors.success, MaterialColors.warning]
start_angle = 0
for i, item in enumerate(self.data):
value = item.get('value', 0)
angle = (value / total) * 360 * 16 # Qt uses 16ths of a degree
color = QColor(colors[i % len(colors)])
painter.setBrush(QBrush(color))
painter.setPen(QColor(MaterialColors.outline))
painter.drawPie(center.x() - radius, center.y() - radius,
radius * 2, radius * 2, start_angle, angle)
start_angle += angle
class TrafficSummaryWidget(QGroupBox):
"""
Widget showing traffic summary statistics.
"""
def __init__(self, parent=None):
super().__init__("Traffic Summary", parent)
self.setup_ui()
self.reset_stats()
def setup_ui(self):
"""Setup summary UI"""
layout = QGridLayout(self)
# Create stat labels
self.total_vehicles_label = QLabel("0")
self.total_violations_label = QLabel("0")
self.avg_speed_label = QLabel("0.0 km/h")
self.peak_hour_label = QLabel("N/A")
# Style the stat values
for label in [self.total_vehicles_label, self.total_violations_label,
self.avg_speed_label, self.peak_hour_label]:
label.setFont(QFont("Segoe UI", 16, QFont.Bold))
label.setStyleSheet(f"color: {MaterialColors.primary};")
# Add to layout
layout.addWidget(QLabel("Total Vehicles:"), 0, 0)
layout.addWidget(self.total_vehicles_label, 0, 1)
layout.addWidget(QLabel("Total Violations:"), 1, 0)
layout.addWidget(self.total_violations_label, 1, 1)
layout.addWidget(QLabel("Average Speed:"), 2, 0)
layout.addWidget(self.avg_speed_label, 2, 1)
layout.addWidget(QLabel("Peak Hour:"), 3, 0)
layout.addWidget(self.peak_hour_label, 3, 1)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
def reset_stats(self):
"""Reset all statistics"""
self.total_vehicles_label.setText("0")
self.total_violations_label.setText("0")
self.avg_speed_label.setText("0.0 km/h")
self.peak_hour_label.setText("N/A")
def update_stats(self, stats):
"""Update statistics display"""
if 'total_vehicles' in stats:
self.total_vehicles_label.setText(str(stats['total_vehicles']))
if 'total_violations' in stats:
self.total_violations_label.setText(str(stats['total_violations']))
if 'avg_speed' in stats:
self.avg_speed_label.setText(f"{stats['avg_speed']:.1f} km/h")
if 'peak_hour' in stats:
self.peak_hour_label.setText(stats['peak_hour'])
class ViolationsTableWidget(QTableWidget):
"""
Table widget for displaying violation records.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setup_table()
def setup_table(self):
"""Setup the violations table"""
# Set columns
columns = ["Time", "Type", "Vehicle", "Location", "Confidence", "Actions"]
self.setColumnCount(len(columns))
self.setHorizontalHeaderLabels(columns)
# Configure table
self.horizontalHeader().setStretchLastSection(True)
self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.setSelectionBehavior(QTableWidget.SelectRows)
self.setAlternatingRowColors(True)
# Apply styling
self.setStyleSheet(FinaleStyles.get_table_style())
def add_violation(self, violation_data):
"""Add a violation record to the table"""
row = self.rowCount()
self.insertRow(row)
# Populate row data
time_str = violation_data.get('timestamp', datetime.now().strftime('%H:%M:%S'))
violation_type = violation_data.get('type', 'Red Light')
vehicle_id = violation_data.get('vehicle_id', 'Unknown')
location = violation_data.get('location', 'Intersection 1')
confidence = violation_data.get('confidence', 0.0)
self.setItem(row, 0, QTableWidgetItem(time_str))
self.setItem(row, 1, QTableWidgetItem(violation_type))
self.setItem(row, 2, QTableWidgetItem(vehicle_id))
self.setItem(row, 3, QTableWidgetItem(location))
self.setItem(row, 4, QTableWidgetItem(f"{confidence:.2f}"))
# Actions button
actions_btn = QPushButton("View Details")
actions_btn.clicked.connect(lambda: self.view_violation_details(violation_data))
self.setCellWidget(row, 5, actions_btn)
# Auto-scroll to new violation
self.scrollToBottom()
def view_violation_details(self, violation_data):
"""View detailed violation information"""
# This could open a detailed dialog
print(f"Viewing violation details: {violation_data}")
class AnalyticsView(QWidget):
"""
Main analytics view with charts, statistics, and violation history.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.analytics_controller = AnalyticsController()
self.setup_ui()
self.analytics_controller.data_updated.connect(self.refresh_analytics)
# Load config if needed
self.config = load_configuration('config.json')
def setup_ui(self):
"""Setup the analytics view UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(16)
# Top controls
controls_layout = QHBoxLayout()
# Date range selection
controls_layout.addWidget(QLabel("Date Range:"))
self.start_date = QDateEdit()
self.start_date.setDate(QDate.currentDate().addDays(-7))
self.start_date.setCalendarPopup(True)
controls_layout.addWidget(self.start_date)
controls_layout.addWidget(QLabel("to"))
self.end_date = QDateEdit()
self.end_date.setDate(QDate.currentDate())
self.end_date.setCalendarPopup(True)
controls_layout.addWidget(self.end_date)
# Time interval
controls_layout.addWidget(QLabel("Interval:"))
self.interval_combo = QComboBox()
self.interval_combo.addItems(["Hourly", "Daily", "Weekly"])
controls_layout.addWidget(self.interval_combo)
# Refresh button
self.refresh_btn = QPushButton(FinaleIcons.get_icon("refresh"), "Refresh")
self.refresh_btn.clicked.connect(self.refresh_data)
controls_layout.addWidget(self.refresh_btn)
controls_layout.addStretch()
layout.addLayout(controls_layout)
# Main content area
content_layout = QHBoxLayout()
# Left panel - Charts
charts_widget = QWidget()
charts_layout = QVBoxLayout(charts_widget)
# Traffic flow chart
self.traffic_chart = AnalyticsChartWidget("Traffic Flow Over Time")
charts_layout.addWidget(self.traffic_chart)
# Violation types chart
self.violations_chart = AnalyticsChartWidget("Violation Types")
charts_layout.addWidget(self.violations_chart)
content_layout.addWidget(charts_widget, 2)
# Right panel - Statistics and table
right_panel = QVBoxLayout()
# Summary statistics
self.summary_widget = TrafficSummaryWidget()
right_panel.addWidget(self.summary_widget)
# Recent violations table
violations_group = QGroupBox("Recent Violations")
violations_layout = QVBoxLayout(violations_group)
self.violations_table = ViolationsTableWidget()
violations_layout.addWidget(self.violations_table)
violations_group.setStyleSheet(FinaleStyles.get_group_box_style())
right_panel.addWidget(violations_group, 1)
content_layout.addLayout(right_panel, 1)
layout.addLayout(content_layout, 1)
# Apply theme
self.apply_theme(True)
# Load initial data
self.refresh_data()
@Slot()
def refresh_data(self):
"""Refresh analytics data"""
print("Refreshing analytics data...")
# Update traffic flow chart (sample data)
traffic_data = [
{'label': '08:00', 'value': 45},
{'label': '09:00', 'value': 67},
{'label': '10:00', 'value': 89},
{'label': '11:00', 'value': 76},
{'label': '12:00', 'value': 92},
{'label': '13:00', 'value': 84},
{'label': '14:00', 'value': 71}
]
self.traffic_chart.set_data(traffic_data, "line")
# Update violations chart
violations_data = [
{'label': 'Red Light', 'value': 12},
{'label': 'Speed', 'value': 8},
{'label': 'Wrong Lane', 'value': 5},
{'label': 'No Helmet', 'value': 3}
]
self.violations_chart.set_data(violations_data, "pie")
# Update summary
summary_stats = {
'total_vehicles': 1247,
'total_violations': 28,
'avg_speed': 35.2,
'peak_hour': '12:00-13:00'
}
self.summary_widget.update_stats(summary_stats)
def refresh_analytics(self):
"""Refresh analytics data from controller"""
data = self.analytics_controller.get_analytics_data()
# Use format_timestamp, format_duration for display
# ... update charts and stats with new data ...
def update_demo_data(self):
"""Update with demo data for demonstration"""
import random
# Simulate new violation
if random.random() < 0.3: # 30% chance
violation = {
'timestamp': datetime.now().strftime('%H:%M:%S'),
'type': random.choice(['Red Light', 'Speed', 'Wrong Lane']),
'vehicle_id': f"VH{random.randint(1000, 9999)}",
'location': f"Intersection {random.randint(1, 5)}",
'confidence': random.uniform(0.7, 0.95)
}
self.violations_table.add_violation(violation)
def add_violation(self, violation_data):
"""Add a new violation (called from main window)"""
self.violations_table.add_violation(violation_data)
def apply_theme(self, dark_mode=True):
"""Apply theme to the view"""
if dark_mode:
self.setStyleSheet(f"""
QWidget {{
background-color: {MaterialColors.surface};
color: {MaterialColors.text_primary};
}}
QPushButton {{
background-color: {MaterialColors.primary};
color: {MaterialColors.text_on_primary};
border: none;
border-radius: 6px;
padding: 8px 16px;
}}
QPushButton:hover {{
background-color: {MaterialColors.primary_variant};
}}
QDateEdit, QComboBox {{
background-color: {MaterialColors.surface_variant};
border: 1px solid {MaterialColors.outline};
border-radius: 4px;
padding: 6px;
}}
""")

View File

@@ -0,0 +1,421 @@
"""
Live View - Real-time detection and monitoring
Connects to existing video controller and live detection logic.
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QFileDialog, QComboBox, QSlider, QSpinBox, QGroupBox,
QGridLayout, QFrame, QSizePolicy, QScrollArea
)
from PySide6.QtCore import Qt, Signal, Slot, QTimer, QSize
from PySide6.QtGui import QPixmap, QPainter, QBrush, QColor, QFont
import cv2
import numpy as np
from pathlib import Path
# Import finale components
from ..styles import FinaleStyles, MaterialColors
from ..icons import FinaleIcons
class VideoDisplayWidget(QLabel):
"""
Advanced video display widget with overlays and interactions.
"""
frame_clicked = Signal(int, int) # x, y coordinates
def __init__(self, parent=None):
super().__init__(parent)
self.setMinimumSize(640, 480)
self.setScaledContents(True)
self.setAlignment(Qt.AlignCenter)
self.setStyleSheet("""
QLabel {
border: 2px solid #424242;
border-radius: 8px;
background-color: #1a1a1a;
}
""")
# State
self.current_pixmap = None
self.overlay_enabled = True
# Default placeholder
self.set_placeholder()
def set_placeholder(self):
"""Set placeholder image when no video is loaded"""
placeholder = QPixmap(640, 480)
placeholder.fill(QColor(26, 26, 26))
painter = QPainter(placeholder)
painter.setPen(QColor(117, 117, 117))
painter.setFont(QFont("Segoe UI", 16))
painter.drawText(placeholder.rect(), Qt.AlignCenter, "No Video Source\nClick to select a file")
painter.end()
self.setPixmap(placeholder)
def update_frame(self, pixmap, detections=None):
"""Update frame with detections overlay"""
if pixmap is None:
return
self.current_pixmap = pixmap
if self.overlay_enabled and detections:
# Draw detection overlays
pixmap = self.add_detection_overlay(pixmap, detections)
self.setPixmap(pixmap)
def add_detection_overlay(self, pixmap, detections):
"""Add detection overlays to pixmap"""
if not detections:
return pixmap
# Create a copy to draw on
overlay_pixmap = QPixmap(pixmap)
painter = QPainter(overlay_pixmap)
# Draw detection boxes
for detection in detections:
# Extract detection info (format depends on backend)
if isinstance(detection, dict):
bbox = detection.get('bbox', [])
confidence = detection.get('confidence', 0.0)
class_name = detection.get('class', 'unknown')
else:
# Handle other detection formats
continue
if len(bbox) >= 4:
x1, y1, x2, y2 = bbox[:4]
# Draw bounding box
painter.setPen(QColor(MaterialColors.primary))
painter.drawRect(int(x1), int(y1), int(x2-x1), int(y2-y1))
# Draw label
label = f"{class_name}: {confidence:.2f}"
painter.setPen(QColor(MaterialColors.text_primary))
painter.drawText(int(x1), int(y1-5), label)
painter.end()
return overlay_pixmap
def mousePressEvent(self, event):
"""Handle mouse click events"""
if event.button() == Qt.LeftButton:
self.frame_clicked.emit(event.x(), event.y())
super().mousePressEvent(event)
class SourceControlWidget(QGroupBox):
"""
Widget for controlling video source (file, camera, stream).
"""
source_changed = Signal(str) # source path/url
def __init__(self, parent=None):
super().__init__("Video Source", parent)
self.setup_ui()
def setup_ui(self):
"""Setup the source control UI"""
layout = QVBoxLayout(self)
# Source type selection
source_layout = QHBoxLayout()
self.source_combo = QComboBox()
self.source_combo.addItems(["Select Source", "Video File", "Camera", "RTSP Stream"])
self.source_combo.currentTextChanged.connect(self.on_source_type_changed)
self.browse_btn = QPushButton(FinaleIcons.get_icon("folder"), "Browse")
self.browse_btn.clicked.connect(self.browse_file)
self.browse_btn.setEnabled(False)
source_layout.addWidget(QLabel("Type:"))
source_layout.addWidget(self.source_combo)
source_layout.addWidget(self.browse_btn)
layout.addLayout(source_layout)
# Source path/URL input
path_layout = QHBoxLayout()
self.path_label = QLabel("Path/URL:")
self.path_display = QLabel("No source selected")
self.path_display.setStyleSheet("QLabel { color: #757575; font-style: italic; }")
path_layout.addWidget(self.path_label)
path_layout.addWidget(self.path_display, 1)
layout.addLayout(path_layout)
# Camera settings (initially hidden)
self.camera_widget = QWidget()
camera_layout = QHBoxLayout(self.camera_widget)
camera_layout.addWidget(QLabel("Camera ID:"))
self.camera_spin = QSpinBox()
self.camera_spin.setRange(0, 10)
camera_layout.addWidget(self.camera_spin)
camera_layout.addStretch()
self.camera_widget.hide()
layout.addWidget(self.camera_widget)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
@Slot(str)
def on_source_type_changed(self, source_type):
"""Handle source type change"""
if source_type == "Video File":
self.browse_btn.setEnabled(True)
self.camera_widget.hide()
elif source_type == "Camera":
self.browse_btn.setEnabled(False)
self.camera_widget.show()
self.path_display.setText(f"Camera {self.camera_spin.value()}")
self.source_changed.emit(str(self.camera_spin.value()))
elif source_type == "RTSP Stream":
self.browse_btn.setEnabled(False)
self.camera_widget.hide()
# Could add RTSP URL input here
else:
self.browse_btn.setEnabled(False)
self.camera_widget.hide()
@Slot()
def browse_file(self):
"""Browse for video file"""
file_path, _ = QFileDialog.getOpenFileName(
self, "Select Video File", "",
"Video Files (*.mp4 *.avi *.mov *.mkv *.wmv);;All Files (*)"
)
if file_path:
self.path_display.setText(file_path)
self.source_changed.emit(file_path)
class DetectionControlWidget(QGroupBox):
"""
Widget for controlling detection parameters.
"""
confidence_changed = Signal(float)
nms_threshold_changed = Signal(float)
def __init__(self, parent=None):
super().__init__("Detection Settings", parent)
self.setup_ui()
def setup_ui(self):
"""Setup detection control UI"""
layout = QGridLayout(self)
# Confidence threshold
layout.addWidget(QLabel("Confidence:"), 0, 0)
self.confidence_slider = QSlider(Qt.Horizontal)
self.confidence_slider.setRange(1, 100)
self.confidence_slider.setValue(30)
self.confidence_slider.valueChanged.connect(self.on_confidence_changed)
self.confidence_label = QLabel("0.30")
self.confidence_label.setMinimumWidth(40)
layout.addWidget(self.confidence_slider, 0, 1)
layout.addWidget(self.confidence_label, 0, 2)
# NMS threshold
layout.addWidget(QLabel("NMS Threshold:"), 1, 0)
self.nms_slider = QSlider(Qt.Horizontal)
self.nms_slider.setRange(1, 100)
self.nms_slider.setValue(45)
self.nms_slider.valueChanged.connect(self.on_nms_changed)
self.nms_label = QLabel("0.45")
self.nms_label.setMinimumWidth(40)
layout.addWidget(self.nms_slider, 1, 1)
layout.addWidget(self.nms_label, 1, 2)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
@Slot(int)
def on_confidence_changed(self, value):
"""Handle confidence threshold change"""
confidence = value / 100.0
self.confidence_label.setText(f"{confidence:.2f}")
self.confidence_changed.emit(confidence)
@Slot(int)
def on_nms_changed(self, value):
"""Handle NMS threshold change"""
nms = value / 100.0
self.nms_label.setText(f"{nms:.2f}")
self.nms_threshold_changed.emit(nms)
class LiveView(QWidget):
"""
Main live detection view.
Displays real-time video with detection overlays and controls.
"""
source_changed = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
self.current_detections = []
def setup_ui(self):
"""Setup the live view UI"""
layout = QHBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(16)
# Main video display area
video_layout = QVBoxLayout()
self.video_widget = VideoDisplayWidget()
self.video_widget.frame_clicked.connect(self.on_frame_clicked)
video_layout.addWidget(self.video_widget, 1)
# Video controls
controls_layout = QHBoxLayout()
self.play_btn = QPushButton(FinaleIcons.get_icon("play"), "")
self.play_btn.setToolTip("Play/Pause")
self.play_btn.setFixedSize(40, 40)
self.stop_btn = QPushButton(FinaleIcons.get_icon("stop"), "")
self.stop_btn.setToolTip("Stop")
self.stop_btn.setFixedSize(40, 40)
self.record_btn = QPushButton(FinaleIcons.get_icon("record"), "")
self.record_btn.setToolTip("Record")
self.record_btn.setFixedSize(40, 40)
self.record_btn.setCheckable(True)
self.snapshot_btn = QPushButton(FinaleIcons.get_icon("camera"), "")
self.snapshot_btn.setToolTip("Take Snapshot")
self.snapshot_btn.setFixedSize(40, 40)
controls_layout.addWidget(self.play_btn)
controls_layout.addWidget(self.stop_btn)
controls_layout.addWidget(self.record_btn)
controls_layout.addWidget(self.snapshot_btn)
controls_layout.addStretch()
# Overlay toggle
self.overlay_btn = QPushButton(FinaleIcons.get_icon("visibility"), "Overlays")
self.overlay_btn.setCheckable(True)
self.overlay_btn.setChecked(True)
self.overlay_btn.toggled.connect(self.toggle_overlays)
controls_layout.addWidget(self.overlay_btn)
video_layout.addLayout(controls_layout)
layout.addLayout(video_layout, 3)
# Right panel for controls
right_panel = QVBoxLayout()
# Source control
self.source_control = SourceControlWidget()
self.source_control.source_changed.connect(self.source_changed.emit)
right_panel.addWidget(self.source_control)
# Detection control
self.detection_control = DetectionControlWidget()
right_panel.addWidget(self.detection_control)
# Detection info
self.info_widget = QGroupBox("Detection Info")
info_layout = QVBoxLayout(self.info_widget)
self.detection_count_label = QLabel("Detections: 0")
self.fps_label = QLabel("FPS: 0.0")
self.resolution_label = QLabel("Resolution: N/A")
info_layout.addWidget(self.detection_count_label)
info_layout.addWidget(self.fps_label)
info_layout.addWidget(self.resolution_label)
self.info_widget.setStyleSheet(FinaleStyles.get_group_box_style())
right_panel.addWidget(self.info_widget)
right_panel.addStretch()
layout.addLayout(right_panel, 1)
# Apply theme
self.apply_theme(True)
def update_frame(self, pixmap, detections=None):
"""Update the video frame with detections"""
if pixmap is None:
return
self.current_detections = detections or []
self.video_widget.update_frame(pixmap, self.current_detections)
# Update detection info
self.detection_count_label.setText(f"Detections: {len(self.current_detections)}")
if pixmap:
size = pixmap.size()
self.resolution_label.setText(f"Resolution: {size.width()}x{size.height()}")
def update_fps(self, fps):
"""Update FPS display"""
self.fps_label.setText(f"FPS: {fps:.1f}")
@Slot(bool)
def toggle_overlays(self, enabled):
"""Toggle detection overlays"""
self.video_widget.overlay_enabled = enabled
# Refresh current frame
if self.video_widget.current_pixmap:
self.video_widget.update_frame(self.video_widget.current_pixmap, self.current_detections)
@Slot(int, int)
def on_frame_clicked(self, x, y):
"""Handle frame click for interaction"""
print(f"Frame clicked at ({x}, {y})")
# Could be used for region selection, etc.
def apply_theme(self, dark_mode=True):
"""Apply theme to the view"""
if dark_mode:
self.setStyleSheet(f"""
QWidget {{
background-color: {MaterialColors.surface};
color: {MaterialColors.text_primary};
}}
QPushButton {{
background-color: {MaterialColors.primary};
color: {MaterialColors.text_on_primary};
border: none;
border-radius: 20px;
padding: 8px;
}}
QPushButton:hover {{
background-color: {MaterialColors.primary_variant};
}}
QPushButton:checked {{
background-color: {MaterialColors.secondary};
}}
""")

View File

@@ -0,0 +1,634 @@
"""
Settings View - Application configuration and preferences
Manages all application settings, model configurations, and system preferences.
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QGroupBox, QGridLayout, QFrame, QScrollArea, QTabWidget,
QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox, QCheckBox,
QSlider, QTextEdit, QFileDialog, QMessageBox, QProgressBar,
QFormLayout, QButtonGroup, QRadioButton
)
from PySide6.QtCore import Qt, Signal, Slot, QTimer, QSettings, QThread, pyqtSignal
from PySide6.QtGui import QFont, QPixmap
import os
import json
import sys
from pathlib import Path
# Import finale components
from ..styles import FinaleStyles, MaterialColors
from ..icons import FinaleIcons
from qt_app_pyside.ui.config_panel import ConfigPanel
from qt_app_pyside.utils.helpers import load_configuration, save_configuration
from qt_app_pyside.utils.helpers import format_timestamp, format_duration
class ModelConfigWidget(QGroupBox):
"""
Widget for configuring AI models and detection parameters.
"""
config_changed = Signal(dict)
def __init__(self, parent=None):
super().__init__("AI Model Configuration", parent)
self.setup_ui()
def setup_ui(self):
"""Setup model configuration UI"""
layout = QFormLayout(self)
# Vehicle detection model
self.vehicle_model_edit = QLineEdit()
self.vehicle_model_edit.setPlaceholderText("Path to vehicle detection model...")
vehicle_browse_btn = QPushButton(FinaleIcons.get_icon("folder"), "")
vehicle_browse_btn.setFixedSize(32, 32)
vehicle_browse_btn.clicked.connect(lambda: self.browse_model("vehicle"))
vehicle_layout = QHBoxLayout()
vehicle_layout.addWidget(self.vehicle_model_edit)
vehicle_layout.addWidget(vehicle_browse_btn)
layout.addRow("Vehicle Model:", vehicle_layout)
# Traffic light detection model
self.traffic_model_edit = QLineEdit()
self.traffic_model_edit.setPlaceholderText("Path to traffic light model...")
traffic_browse_btn = QPushButton(FinaleIcons.get_icon("folder"), "")
traffic_browse_btn.setFixedSize(32, 32)
traffic_browse_btn.clicked.connect(lambda: self.browse_model("traffic"))
traffic_layout = QHBoxLayout()
traffic_layout.addWidget(self.traffic_model_edit)
traffic_layout.addWidget(traffic_browse_btn)
layout.addRow("Traffic Light Model:", traffic_layout)
# Detection parameters
self.confidence_spin = QDoubleSpinBox()
self.confidence_spin.setRange(0.1, 1.0)
self.confidence_spin.setSingleStep(0.05)
self.confidence_spin.setValue(0.3)
self.confidence_spin.setSuffix(" (30%)")
layout.addRow("Confidence Threshold:", self.confidence_spin)
self.nms_spin = QDoubleSpinBox()
self.nms_spin.setRange(0.1, 1.0)
self.nms_spin.setSingleStep(0.05)
self.nms_spin.setValue(0.45)
layout.addRow("NMS Threshold:", self.nms_spin)
self.max_detections_spin = QSpinBox()
self.max_detections_spin.setRange(10, 1000)
self.max_detections_spin.setValue(100)
layout.addRow("Max Detections:", self.max_detections_spin)
# Device selection
self.device_combo = QComboBox()
self.device_combo.addItems(["CPU", "GPU", "AUTO"])
layout.addRow("Device:", self.device_combo)
# Model optimization
self.optimize_check = QCheckBox("Enable Model Optimization")
self.optimize_check.setChecked(True)
layout.addRow(self.optimize_check)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
@Slot()
def browse_model(self, model_type):
"""Browse for model file"""
file_path, _ = QFileDialog.getOpenFileName(
self, f"Select {model_type.title()} Model", "",
"Model Files (*.xml *.onnx *.pt *.bin);;All Files (*)"
)
if file_path:
if model_type == "vehicle":
self.vehicle_model_edit.setText(file_path)
elif model_type == "traffic":
self.traffic_model_edit.setText(file_path)
def get_config(self):
"""Get current model configuration"""
return {
'vehicle_model': self.vehicle_model_edit.text(),
'traffic_model': self.traffic_model_edit.text(),
'confidence_threshold': self.confidence_spin.value(),
'nms_threshold': self.nms_spin.value(),
'max_detections': self.max_detections_spin.value(),
'device': self.device_combo.currentText(),
'optimize_model': self.optimize_check.isChecked()
}
def set_config(self, config):
"""Set model configuration"""
self.vehicle_model_edit.setText(config.get('vehicle_model', ''))
self.traffic_model_edit.setText(config.get('traffic_model', ''))
self.confidence_spin.setValue(config.get('confidence_threshold', 0.3))
self.nms_spin.setValue(config.get('nms_threshold', 0.45))
self.max_detections_spin.setValue(config.get('max_detections', 100))
self.device_combo.setCurrentText(config.get('device', 'CPU'))
self.optimize_check.setChecked(config.get('optimize_model', True))
class ViolationConfigWidget(QGroupBox):
"""
Widget for configuring violation detection parameters.
"""
def __init__(self, parent=None):
super().__init__("Violation Detection", parent)
self.setup_ui()
def setup_ui(self):
"""Setup violation configuration UI"""
layout = QFormLayout(self)
# Red light violation
self.red_light_check = QCheckBox("Enable Red Light Detection")
self.red_light_check.setChecked(True)
layout.addRow(self.red_light_check)
self.red_light_sensitivity = QSlider(Qt.Horizontal)
self.red_light_sensitivity.setRange(1, 10)
self.red_light_sensitivity.setValue(5)
layout.addRow("Red Light Sensitivity:", self.red_light_sensitivity)
# Speed violation
self.speed_check = QCheckBox("Enable Speed Detection")
self.speed_check.setChecked(True)
layout.addRow(self.speed_check)
self.speed_limit_spin = QSpinBox()
self.speed_limit_spin.setRange(10, 200)
self.speed_limit_spin.setValue(50)
self.speed_limit_spin.setSuffix(" km/h")
layout.addRow("Speed Limit:", self.speed_limit_spin)
self.speed_tolerance_spin = QSpinBox()
self.speed_tolerance_spin.setRange(0, 20)
self.speed_tolerance_spin.setValue(5)
self.speed_tolerance_spin.setSuffix(" km/h")
layout.addRow("Speed Tolerance:", self.speed_tolerance_spin)
# Wrong lane detection
self.wrong_lane_check = QCheckBox("Enable Wrong Lane Detection")
self.wrong_lane_check.setChecked(True)
layout.addRow(self.wrong_lane_check)
# Helmet detection
self.helmet_check = QCheckBox("Enable Helmet Detection")
self.helmet_check.setChecked(False)
layout.addRow(self.helmet_check)
# Violation zone setup
self.zone_setup_btn = QPushButton(FinaleIcons.get_icon("map"), "Setup Violation Zones")
layout.addRow(self.zone_setup_btn)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
class UIPreferencesWidget(QGroupBox):
"""
Widget for UI preferences and appearance settings.
"""
theme_changed = Signal(bool) # dark_mode
def __init__(self, parent=None):
super().__init__("User Interface", parent)
self.setup_ui()
def setup_ui(self):
"""Setup UI preferences"""
layout = QFormLayout(self)
# Theme selection
theme_group = QButtonGroup(self)
self.dark_radio = QRadioButton("Dark Theme")
self.light_radio = QRadioButton("Light Theme")
self.auto_radio = QRadioButton("Auto (System)")
self.dark_radio.setChecked(True) # Default to dark
theme_group.addButton(self.dark_radio)
theme_group.addButton(self.light_radio)
theme_group.addButton(self.auto_radio)
theme_layout = QVBoxLayout()
theme_layout.addWidget(self.dark_radio)
theme_layout.addWidget(self.light_radio)
theme_layout.addWidget(self.auto_radio)
layout.addRow("Theme:", theme_layout)
# Language selection
self.language_combo = QComboBox()
self.language_combo.addItems(["English", "Español", "Français", "Deutsch", "العربية"])
layout.addRow("Language:", self.language_combo)
# Font size
self.font_size_spin = QSpinBox()
self.font_size_spin.setRange(8, 16)
self.font_size_spin.setValue(9)
layout.addRow("Font Size:", self.font_size_spin)
# Animations
self.animations_check = QCheckBox("Enable Animations")
self.animations_check.setChecked(True)
layout.addRow(self.animations_check)
# Sound notifications
self.sound_check = QCheckBox("Sound Notifications")
self.sound_check.setChecked(True)
layout.addRow(self.sound_check)
# Auto-save
self.autosave_check = QCheckBox("Auto-save Configuration")
self.autosave_check.setChecked(True)
layout.addRow(self.autosave_check)
# Update interval
self.update_interval_spin = QSpinBox()
self.update_interval_spin.setRange(100, 5000)
self.update_interval_spin.setValue(1000)
self.update_interval_spin.setSuffix(" ms")
layout.addRow("Update Interval:", self.update_interval_spin)
# Connect theme signals
self.dark_radio.toggled.connect(lambda checked: self.theme_changed.emit(True) if checked else None)
self.light_radio.toggled.connect(lambda checked: self.theme_changed.emit(False) if checked else None)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
class PerformanceWidget(QGroupBox):
"""
Widget for performance and system settings.
"""
def __init__(self, parent=None):
super().__init__("Performance", parent)
self.setup_ui()
def setup_ui(self):
"""Setup performance settings"""
layout = QFormLayout(self)
# Processing threads
self.threads_spin = QSpinBox()
self.threads_spin.setRange(1, 16)
self.threads_spin.setValue(4)
layout.addRow("Processing Threads:", self.threads_spin)
# Frame buffer size
self.buffer_size_spin = QSpinBox()
self.buffer_size_spin.setRange(1, 100)
self.buffer_size_spin.setValue(10)
layout.addRow("Frame Buffer Size:", self.buffer_size_spin)
# Memory limit
self.memory_limit_spin = QSpinBox()
self.memory_limit_spin.setRange(512, 8192)
self.memory_limit_spin.setValue(2048)
self.memory_limit_spin.setSuffix(" MB")
layout.addRow("Memory Limit:", self.memory_limit_spin)
# GPU acceleration
self.gpu_check = QCheckBox("Enable GPU Acceleration")
self.gpu_check.setChecked(False)
layout.addRow(self.gpu_check)
# Performance mode
self.performance_combo = QComboBox()
self.performance_combo.addItems(["Balanced", "Performance", "Power Save"])
layout.addRow("Performance Mode:", self.performance_combo)
# Logging level
self.logging_combo = QComboBox()
self.logging_combo.addItems(["DEBUG", "INFO", "WARNING", "ERROR"])
self.logging_combo.setCurrentText("INFO")
layout.addRow("Logging Level:", self.logging_combo)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
class DataManagementWidget(QGroupBox):
"""
Widget for data storage and export settings.
"""
def __init__(self, parent=None):
super().__init__("Data Management", parent)
self.setup_ui()
def setup_ui(self):
"""Setup data management settings"""
layout = QFormLayout(self)
# Data directory
self.data_dir_edit = QLineEdit()
self.data_dir_edit.setPlaceholderText("Data storage directory...")
data_browse_btn = QPushButton(FinaleIcons.get_icon("folder"), "")
data_browse_btn.setFixedSize(32, 32)
data_browse_btn.clicked.connect(self.browse_data_directory)
data_layout = QHBoxLayout()
data_layout.addWidget(self.data_dir_edit)
data_layout.addWidget(data_browse_btn)
layout.addRow("Data Directory:", data_layout)
# Auto-export
self.auto_export_check = QCheckBox("Auto-export Violations")
layout.addRow(self.auto_export_check)
# Export format
self.export_format_combo = QComboBox()
self.export_format_combo.addItems(["JSON", "CSV", "XML", "PDF"])
layout.addRow("Export Format:", self.export_format_combo)
# Data retention
self.retention_spin = QSpinBox()
self.retention_spin.setRange(1, 365)
self.retention_spin.setValue(30)
self.retention_spin.setSuffix(" days")
layout.addRow("Data Retention:", self.retention_spin)
# Backup settings
self.backup_check = QCheckBox("Enable Automatic Backup")
layout.addRow(self.backup_check)
self.backup_interval_combo = QComboBox()
self.backup_interval_combo.addItems(["Daily", "Weekly", "Monthly"])
layout.addRow("Backup Interval:", self.backup_interval_combo)
# Database cleanup
cleanup_btn = QPushButton(FinaleIcons.get_icon("delete"), "Cleanup Old Data")
layout.addRow(cleanup_btn)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
@Slot()
def browse_data_directory(self):
"""Browse for data directory"""
directory = QFileDialog.getExistingDirectory(
self, "Select Data Directory", self.data_dir_edit.text()
)
if directory:
self.data_dir_edit.setText(directory)
class SettingsView(QWidget):
"""
Main settings view with tabbed configuration sections.
"""
settings_changed = Signal(dict)
def __init__(self, parent=None):
super().__init__(parent)
self.config = load_configuration('config.json')
# Add configuration panel from original
self.config_panel = ConfigPanel()
self.settings = QSettings("Finale", "TrafficMonitoring")
self.setup_ui()
self.load_settings()
def setup_ui(self):
"""Setup the settings view UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(16)
# Header
header_layout = QHBoxLayout()
title_label = QLabel("Settings")
title_label.setFont(QFont("Segoe UI", 18, QFont.Bold))
# Action buttons
self.reset_btn = QPushButton(FinaleIcons.get_icon("refresh"), "Reset to Defaults")
self.reset_btn.clicked.connect(self.reset_to_defaults)
self.export_btn = QPushButton(FinaleIcons.get_icon("export"), "Export Settings")
self.export_btn.clicked.connect(self.export_settings)
self.import_btn = QPushButton(FinaleIcons.get_icon("import"), "Import Settings")
self.import_btn.clicked.connect(self.import_settings)
header_layout.addWidget(title_label)
header_layout.addStretch()
header_layout.addWidget(self.reset_btn)
header_layout.addWidget(self.export_btn)
header_layout.addWidget(self.import_btn)
layout.addLayout(header_layout)
# Settings tabs
self.tabs = QTabWidget()
# Create configuration widgets
self.model_config = ModelConfigWidget()
self.violation_config = ViolationConfigWidget()
self.ui_preferences = UIPreferencesWidget()
self.performance_config = PerformanceWidget()
self.data_management = DataManagementWidget()
# Add tabs
self.tabs.addTab(self.model_config, FinaleIcons.get_icon("model"), "AI Models")
self.tabs.addTab(self.violation_config, FinaleIcons.get_icon("warning"), "Violations")
self.tabs.addTab(self.ui_preferences, FinaleIcons.get_icon("palette"), "Interface")
self.tabs.addTab(self.performance_config, FinaleIcons.get_icon("speed"), "Performance")
self.tabs.addTab(self.data_management, FinaleIcons.get_icon("database"), "Data")
# Style tabs
self.tabs.setStyleSheet(FinaleStyles.get_tab_widget_style())
layout.addWidget(self.tabs, 1)
# Bottom action bar
action_layout = QHBoxLayout()
self.apply_btn = QPushButton(FinaleIcons.get_icon("check"), "Apply")
self.apply_btn.clicked.connect(self.apply_settings)
self.save_btn = QPushButton(FinaleIcons.get_icon("save"), "Save")
self.save_btn.clicked.connect(self.save_settings)
self.cancel_btn = QPushButton(FinaleIcons.get_icon("close"), "Cancel")
self.cancel_btn.clicked.connect(self.cancel_changes)
action_layout.addStretch()
action_layout.addWidget(self.apply_btn)
action_layout.addWidget(self.save_btn)
action_layout.addWidget(self.cancel_btn)
layout.addLayout(action_layout)
# Connect signals
self.ui_preferences.theme_changed.connect(self.on_theme_changed)
# Apply theme
self.apply_theme(True)
def load_settings(self):
"""Load settings from QSettings"""
# Load model configuration
model_config = {
'vehicle_model': self.settings.value('model/vehicle_model', ''),
'traffic_model': self.settings.value('model/traffic_model', ''),
'confidence_threshold': self.settings.value('model/confidence_threshold', 0.3, float),
'nms_threshold': self.settings.value('model/nms_threshold', 0.45, float),
'max_detections': self.settings.value('model/max_detections', 100, int),
'device': self.settings.value('model/device', 'CPU'),
'optimize_model': self.settings.value('model/optimize_model', True, bool)
}
self.model_config.set_config(model_config)
# Load UI preferences
dark_mode = self.settings.value('ui/dark_mode', True, bool)
if dark_mode:
self.ui_preferences.dark_radio.setChecked(True)
else:
self.ui_preferences.light_radio.setChecked(True)
@Slot()
def apply_settings(self):
"""Apply current settings"""
settings_data = self.get_all_settings()
self.settings_changed.emit(settings_data)
@Slot()
def save_settings(self):
"""Save settings to QSettings"""
# Save model configuration
model_config = self.model_config.get_config()
for key, value in model_config.items():
self.settings.setValue(f'model/{key}', value)
# Save UI preferences
self.settings.setValue('ui/dark_mode', self.ui_preferences.dark_radio.isChecked())
# Sync settings
self.settings.sync()
QMessageBox.information(self, "Settings Saved", "Settings have been saved successfully.")
save_configuration(settings_data, 'config.json')
@Slot()
def cancel_changes(self):
"""Cancel changes and reload settings"""
self.load_settings()
@Slot()
def reset_to_defaults(self):
"""Reset all settings to defaults"""
reply = QMessageBox.question(
self, "Reset Settings",
"Are you sure you want to reset all settings to defaults?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.settings.clear()
self.load_settings()
@Slot()
def export_settings(self):
"""Export settings to file"""
file_path, _ = QFileDialog.getSaveFileName(
self, "Export Settings", "",
"JSON Files (*.json);;All Files (*)"
)
if file_path:
settings_data = self.get_all_settings()
try:
with open(file_path, 'w') as f:
json.dump(settings_data, f, indent=2)
QMessageBox.information(self, "Export Successful", "Settings exported successfully.")
except Exception as e:
QMessageBox.critical(self, "Export Error", f"Failed to export settings:\n{str(e)}")
@Slot()
def import_settings(self):
"""Import settings from file"""
file_path, _ = QFileDialog.getOpenFileName(
self, "Import Settings", "",
"JSON Files (*.json);;All Files (*)"
)
if file_path:
try:
with open(file_path, 'r') as f:
settings_data = json.load(f)
# Apply imported settings
self.apply_imported_settings(settings_data)
QMessageBox.information(self, "Import Successful", "Settings imported successfully.")
except Exception as e:
QMessageBox.critical(self, "Import Error", f"Failed to import settings:\n{str(e)}")
def get_all_settings(self):
"""Get all current settings as dictionary"""
return {
'model': self.model_config.get_config(),
'ui': {
'dark_mode': self.ui_preferences.dark_radio.isChecked(),
'language': self.ui_preferences.language_combo.currentText(),
'font_size': self.ui_preferences.font_size_spin.value(),
'animations': self.ui_preferences.animations_check.isChecked(),
'sound': self.ui_preferences.sound_check.isChecked()
}
}
def apply_imported_settings(self, settings_data):
"""Apply imported settings data"""
if 'model' in settings_data:
self.model_config.set_config(settings_data['model'])
if 'ui' in settings_data:
ui_settings = settings_data['ui']
if 'dark_mode' in ui_settings:
if ui_settings['dark_mode']:
self.ui_preferences.dark_radio.setChecked(True)
else:
self.ui_preferences.light_radio.setChecked(True)
@Slot(bool)
def on_theme_changed(self, dark_mode):
"""Handle theme change"""
self.apply_theme(dark_mode)
def apply_theme(self, dark_mode=True):
"""Apply theme to the view"""
if dark_mode:
self.setStyleSheet(f"""
QWidget {{
background-color: {MaterialColors.surface};
color: {MaterialColors.text_primary};
}}
QPushButton {{
background-color: {MaterialColors.primary};
color: {MaterialColors.text_on_primary};
border: none;
border-radius: 6px;
padding: 8px 16px;
}}
QPushButton:hover {{
background-color: {MaterialColors.primary_variant};
}}
""")
def display_timestamp(self, ts):
return format_timestamp(ts)
def display_duration(self, seconds):
return format_duration(seconds)

View File

@@ -0,0 +1,609 @@
"""
Violations View - Violation management and history
Displays violation records, details, and management tools.
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QGroupBox, QGridLayout, QFrame, QScrollArea, QTabWidget,
QTableWidget, QTableWidgetItem, QHeaderView, QDateEdit,
QComboBox, QSpinBox, QLineEdit, QTextEdit, QDialog,
QDialogButtonBox, QSplitter, QListWidget, QListWidgetItem
)
from PySide6.QtCore import Qt, Signal, Slot, QTimer, QDate, QSize
from PySide6.QtGui import QPixmap, QPainter, QBrush, QColor, QFont, QIcon
from datetime import datetime, timedelta
import json
import os
# Import finale components
from ..styles import FinaleStyles, MaterialColors
from ..icons import FinaleIcons
from qt_app_pyside.utils.helpers import save_configuration, create_export_csv, create_export_json
from qt_app_pyside.utils.annotation_utils import draw_detections
from qt_app_pyside.utils.enhanced_annotation_utils import enhanced_draw_detections
from qt_app_pyside.ui.export_tab import ExportTab
from qt_app_pyside.ui.violations_tab import ViolationsTab as OriginalViolationsTab
class ViolationDetailDialog(QDialog):
"""
Dialog for viewing detailed violation information.
"""
def __init__(self, violation_data, parent=None):
super().__init__(parent)
self.violation_data = violation_data
self.setup_ui()
def setup_ui(self):
"""Setup the detail dialog UI"""
self.setWindowTitle("Violation Details")
self.setMinimumSize(600, 500)
layout = QVBoxLayout(self)
# Header with violation type and timestamp
header_frame = QFrame()
header_frame.setStyleSheet(f"""
QFrame {{
background-color: {MaterialColors.primary};
color: {MaterialColors.text_on_primary};
border-radius: 8px;
padding: 16px;
}}
""")
header_layout = QHBoxLayout(header_frame)
violation_type = self.violation_data.get('type', 'Unknown')
timestamp = self.violation_data.get('timestamp', 'Unknown')
type_label = QLabel(violation_type)
type_label.setFont(QFont("Segoe UI", 16, QFont.Bold))
time_label = QLabel(timestamp)
time_label.setFont(QFont("Segoe UI", 12))
header_layout.addWidget(type_label)
header_layout.addStretch()
header_layout.addWidget(time_label)
layout.addWidget(header_frame)
# Main content area
content_splitter = QSplitter(Qt.Horizontal)
# Left side - Image/Video
image_group = QGroupBox("Evidence")
image_layout = QVBoxLayout(image_group)
self.image_label = QLabel()
self.image_label.setMinimumSize(300, 200)
self.image_label.setStyleSheet("""
QLabel {
border: 2px solid #424242;
border-radius: 8px;
background-color: #1a1a1a;
}
""")
self.image_label.setAlignment(Qt.AlignCenter)
self.image_label.setText("No image available")
# Load image if available
image_path = self.violation_data.get('image_path')
if image_path and os.path.exists(image_path):
pixmap = QPixmap(image_path)
if not pixmap.isNull():
scaled_pixmap = pixmap.scaled(300, 200, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.image_label.setPixmap(scaled_pixmap)
image_layout.addWidget(self.image_label)
# Image controls
image_controls = QHBoxLayout()
save_image_btn = QPushButton(FinaleIcons.get_icon("save"), "Save Image")
view_full_btn = QPushButton(FinaleIcons.get_icon("zoom_in"), "View Full")
image_controls.addWidget(save_image_btn)
image_controls.addWidget(view_full_btn)
image_controls.addStretch()
image_layout.addLayout(image_controls)
content_splitter.addWidget(image_group)
# Right side - Details
details_group = QGroupBox("Details")
details_layout = QGridLayout(details_group)
# Violation details
details = [
("Vehicle ID:", self.violation_data.get('vehicle_id', 'Unknown')),
("Location:", self.violation_data.get('location', 'Unknown')),
("Confidence:", f"{self.violation_data.get('confidence', 0.0):.2f}"),
("Speed:", f"{self.violation_data.get('speed', 0.0):.1f} km/h"),
("Lane:", self.violation_data.get('lane', 'Unknown')),
("Weather:", self.violation_data.get('weather', 'Unknown')),
("Officer ID:", self.violation_data.get('officer_id', 'N/A')),
("Status:", self.violation_data.get('status', 'Pending'))
]
for i, (label, value) in enumerate(details):
label_widget = QLabel(label)
label_widget.setFont(QFont("Segoe UI", 9, QFont.Bold))
value_widget = QLabel(str(value))
value_widget.setStyleSheet(f"color: {MaterialColors.text_secondary};")
details_layout.addWidget(label_widget, i, 0)
details_layout.addWidget(value_widget, i, 1)
# Notes section
notes_label = QLabel("Notes:")
notes_label.setFont(QFont("Segoe UI", 9, QFont.Bold))
details_layout.addWidget(notes_label, len(details), 0, 1, 2)
self.notes_edit = QTextEdit()
self.notes_edit.setMaximumHeight(100)
self.notes_edit.setPlainText(self.violation_data.get('notes', ''))
details_layout.addWidget(self.notes_edit, len(details) + 1, 0, 1, 2)
content_splitter.addWidget(details_group)
layout.addWidget(content_splitter)
# Action buttons
button_layout = QHBoxLayout()
export_btn = QPushButton(FinaleIcons.get_icon("export"), "Export Report")
delete_btn = QPushButton(FinaleIcons.get_icon("delete"), "Delete")
delete_btn.setStyleSheet(f"background-color: {MaterialColors.error};")
button_layout.addWidget(export_btn)
button_layout.addWidget(delete_btn)
button_layout.addStretch()
# Standard dialog buttons
button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Close)
button_box.accepted.connect(self.save_changes)
button_box.rejected.connect(self.reject)
button_layout.addWidget(button_box)
layout.addLayout(button_layout)
# Apply styling
self.setStyleSheet(FinaleStyles.get_dialog_style())
@Slot()
def save_changes(self):
"""Save changes to violation data"""
# Update notes
self.violation_data['notes'] = self.notes_edit.toPlainText()
# Here you would save to database/file
self.accept()
class ViolationFilterWidget(QGroupBox):
"""
Widget for filtering violations by various criteria.
"""
filter_changed = Signal(dict)
def __init__(self, parent=None):
super().__init__("Filter Violations", parent)
self.setup_ui()
def setup_ui(self):
"""Setup filter UI"""
layout = QGridLayout(self)
# Date range
layout.addWidget(QLabel("Date From:"), 0, 0)
self.date_from = QDateEdit()
self.date_from.setDate(QDate.currentDate().addDays(-30))
self.date_from.setCalendarPopup(True)
layout.addWidget(self.date_from, 0, 1)
layout.addWidget(QLabel("Date To:"), 0, 2)
self.date_to = QDateEdit()
self.date_to.setDate(QDate.currentDate())
self.date_to.setCalendarPopup(True)
layout.addWidget(self.date_to, 0, 3)
# Violation type
layout.addWidget(QLabel("Type:"), 1, 0)
self.type_combo = QComboBox()
self.type_combo.addItems(["All Types", "Red Light", "Speed", "Wrong Lane", "No Helmet", "Other"])
layout.addWidget(self.type_combo, 1, 1)
# Status
layout.addWidget(QLabel("Status:"), 1, 2)
self.status_combo = QComboBox()
self.status_combo.addItems(["All Status", "Pending", "Reviewed", "Closed", "Disputed"])
layout.addWidget(self.status_combo, 1, 3)
# Location
layout.addWidget(QLabel("Location:"), 2, 0)
self.location_edit = QLineEdit()
self.location_edit.setPlaceholderText("Enter location...")
layout.addWidget(self.location_edit, 2, 1)
# Confidence threshold
layout.addWidget(QLabel("Min Confidence:"), 2, 2)
self.confidence_spin = QSpinBox()
self.confidence_spin.setRange(0, 100)
self.confidence_spin.setValue(50)
self.confidence_spin.setSuffix("%")
layout.addWidget(self.confidence_spin, 2, 3)
# Apply button
self.apply_btn = QPushButton(FinaleIcons.get_icon("filter"), "Apply Filter")
self.apply_btn.clicked.connect(self.apply_filter)
layout.addWidget(self.apply_btn, 3, 0, 1, 4)
# Connect signals for auto-update
self.date_from.dateChanged.connect(self.on_filter_changed)
self.date_to.dateChanged.connect(self.on_filter_changed)
self.type_combo.currentTextChanged.connect(self.on_filter_changed)
self.status_combo.currentTextChanged.connect(self.on_filter_changed)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
@Slot()
def apply_filter(self):
"""Apply current filter settings"""
self.on_filter_changed()
def on_filter_changed(self):
"""Emit filter changed signal with current settings"""
filter_data = {
'date_from': self.date_from.date().toPython(),
'date_to': self.date_to.date().toPython(),
'type': self.type_combo.currentText(),
'status': self.status_combo.currentText(),
'location': self.location_edit.text(),
'min_confidence': self.confidence_spin.value() / 100.0
}
self.filter_changed.emit(filter_data)
class ViolationListWidget(QWidget):
"""
Widget displaying violation list with thumbnails and quick info.
"""
violation_selected = Signal(dict)
def __init__(self, parent=None):
super().__init__(parent)
self.violations = []
self.setup_ui()
def setup_ui(self):
"""Setup violation list UI"""
layout = QVBoxLayout(self)
# Header
header_layout = QHBoxLayout()
self.count_label = QLabel("0 violations")
self.count_label.setFont(QFont("Segoe UI", 12, QFont.Bold))
self.sort_combo = QComboBox()
self.sort_combo.addItems(["Sort by Time", "Sort by Type", "Sort by Confidence", "Sort by Status"])
self.sort_combo.currentTextChanged.connect(self.sort_violations)
header_layout.addWidget(self.count_label)
header_layout.addStretch()
header_layout.addWidget(QLabel("Sort:"))
header_layout.addWidget(self.sort_combo)
layout.addLayout(header_layout)
# Violations list
self.list_widget = QListWidget()
self.list_widget.itemClicked.connect(self.on_item_clicked)
self.list_widget.setStyleSheet(FinaleStyles.get_list_style())
layout.addWidget(self.list_widget)
def add_violation(self, violation_data):
"""Add a violation to the list"""
self.violations.append(violation_data)
self.update_list()
def set_violations(self, violations):
"""Set the complete list of violations"""
self.violations = violations
self.update_list()
def update_list(self):
"""Update the violation list display"""
self.list_widget.clear()
for violation in self.violations:
item = QListWidgetItem()
# Create custom widget for violation item
item_widget = self.create_violation_item_widget(violation)
item.setSizeHint(item_widget.sizeHint())
self.list_widget.addItem(item)
self.list_widget.setItemWidget(item, item_widget)
# Update count
self.count_label.setText(f"{len(self.violations)} violations")
def create_violation_item_widget(self, violation):
"""Create a custom widget for a violation list item"""
widget = QWidget()
layout = QHBoxLayout(widget)
layout.setContentsMargins(8, 8, 8, 8)
# Thumbnail (placeholder for now)
thumbnail = QLabel()
thumbnail.setFixedSize(80, 60)
thumbnail.setStyleSheet("""
QLabel {
border: 1px solid #424242;
border-radius: 4px;
background-color: #2d2d2d;
}
""")
thumbnail.setAlignment(Qt.AlignCenter)
thumbnail.setText("IMG")
layout.addWidget(thumbnail)
# Violation info
info_layout = QVBoxLayout()
# Title line
title_layout = QHBoxLayout()
type_label = QLabel(violation.get('type', 'Unknown'))
type_label.setFont(QFont("Segoe UI", 11, QFont.Bold))
time_label = QLabel(violation.get('timestamp', ''))
time_label.setStyleSheet(f"color: {MaterialColors.text_secondary}; font-size: 10px;")
title_layout.addWidget(type_label)
title_layout.addStretch()
title_layout.addWidget(time_label)
info_layout.addLayout(title_layout)
# Details line
details = f"Vehicle: {violation.get('vehicle_id', 'Unknown')} | Location: {violation.get('location', 'Unknown')}"
details_label = QLabel(details)
details_label.setStyleSheet(f"color: {MaterialColors.text_secondary}; font-size: 9px;")
info_layout.addWidget(details_label)
# Confidence and status
status_layout = QHBoxLayout()
confidence = violation.get('confidence', 0.0)
confidence_label = QLabel(f"Confidence: {confidence:.2f}")
confidence_label.setStyleSheet(f"color: {MaterialColors.primary}; font-size: 9px;")
status = violation.get('status', 'Pending')
status_label = QLabel(status)
status_color = {
'Pending': MaterialColors.warning,
'Reviewed': MaterialColors.primary,
'Closed': MaterialColors.success,
'Disputed': MaterialColors.error
}.get(status, MaterialColors.text_secondary)
status_label.setStyleSheet(f"color: {status_color}; font-size: 9px; font-weight: bold;")
status_layout.addWidget(confidence_label)
status_layout.addStretch()
status_layout.addWidget(status_label)
info_layout.addLayout(status_layout)
layout.addLayout(info_layout, 1)
# Store violation data in widget
widget.violation_data = violation
return widget
def sort_violations(self, sort_by):
"""Sort violations by the specified criteria"""
if sort_by == "Sort by Time":
self.violations.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
elif sort_by == "Sort by Type":
self.violations.sort(key=lambda x: x.get('type', ''))
elif sort_by == "Sort by Confidence":
self.violations.sort(key=lambda x: x.get('confidence', 0.0), reverse=True)
elif sort_by == "Sort by Status":
self.violations.sort(key=lambda x: x.get('status', ''))
self.update_list()
@Slot(QListWidgetItem)
def on_item_clicked(self, item):
"""Handle violation item click"""
item_widget = self.list_widget.itemWidget(item)
if hasattr(item_widget, 'violation_data'):
self.violation_selected.emit(item_widget.violation_data)
class ViolationsView(QWidget):
"""
Main violations view with filtering, list, and detail management.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
self.load_sample_data()
self.save_config = save_configuration
self.export_csv = create_export_csv
self.export_json = create_export_json
self.draw_detections = draw_detections
self.enhanced_draw_detections = enhanced_draw_detections
# Add export functionality from original export_tab
self.export_handler = ExportTab()
def setup_ui(self):
"""Setup the violations view UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(16)
# Filter widget
self.filter_widget = ViolationFilterWidget()
self.filter_widget.filter_changed.connect(self.apply_filter)
layout.addWidget(self.filter_widget)
# Main content area
content_splitter = QSplitter(Qt.Horizontal)
# Left side - Violation list
self.violation_list = ViolationListWidget()
self.violation_list.violation_selected.connect(self.show_violation_details)
content_splitter.addWidget(self.violation_list)
# Right side - Quick actions and summary
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
# Quick actions
actions_group = QGroupBox("Quick Actions")
actions_layout = QVBoxLayout(actions_group)
export_all_btn = QPushButton(FinaleIcons.get_icon("export"), "Export All")
export_filtered_btn = QPushButton(FinaleIcons.get_icon("filter"), "Export Filtered")
delete_selected_btn = QPushButton(FinaleIcons.get_icon("delete"), "Delete Selected")
mark_reviewed_btn = QPushButton(FinaleIcons.get_icon("check"), "Mark as Reviewed")
actions_layout.addWidget(export_all_btn)
actions_layout.addWidget(export_filtered_btn)
actions_layout.addWidget(delete_selected_btn)
actions_layout.addWidget(mark_reviewed_btn)
actions_group.setStyleSheet(FinaleStyles.get_group_box_style())
right_layout.addWidget(actions_group)
# Summary statistics
summary_group = QGroupBox("Summary")
summary_layout = QGridLayout(summary_group)
self.total_label = QLabel("Total: 0")
self.pending_label = QLabel("Pending: 0")
self.reviewed_label = QLabel("Reviewed: 0")
self.closed_label = QLabel("Closed: 0")
summary_layout.addWidget(self.total_label, 0, 0)
summary_layout.addWidget(self.pending_label, 0, 1)
summary_layout.addWidget(self.reviewed_label, 1, 0)
summary_layout.addWidget(self.closed_label, 1, 1)
summary_group.setStyleSheet(FinaleStyles.get_group_box_style())
right_layout.addWidget(summary_group)
right_layout.addStretch()
content_splitter.addWidget(right_panel)
# Set splitter proportions
content_splitter.setSizes([700, 300])
layout.addWidget(content_splitter, 1)
# Apply theme
self.apply_theme(True)
def load_sample_data(self):
"""Load sample violation data for demonstration"""
sample_violations = [
{
'timestamp': '14:23:15',
'type': 'Red Light',
'vehicle_id': 'VH1234',
'location': 'Main St & 1st Ave',
'confidence': 0.92,
'status': 'Pending',
'speed': 45.2,
'lane': 'Left Turn',
'notes': 'Clear violation captured on camera.'
},
{
'timestamp': '13:45:32',
'type': 'Speed',
'vehicle_id': 'VH5678',
'location': 'Highway 101',
'confidence': 0.87,
'status': 'Reviewed',
'speed': 78.5,
'lane': 'Right',
'notes': 'Speed limit 60 km/h, vehicle traveling at 78.5 km/h.'
},
{
'timestamp': '12:15:48',
'type': 'Wrong Lane',
'vehicle_id': 'VH9012',
'location': 'Oak St Bridge',
'confidence': 0.76,
'status': 'Closed',
'speed': 32.1,
'lane': 'Bus Lane',
'notes': 'Vehicle in bus-only lane during restricted hours.'
}
]
self.violation_list.set_violations(sample_violations)
self.update_summary()
def add_violation(self, violation_data):
"""Add a new violation (called from main window)"""
self.violation_list.add_violation(violation_data)
self.update_summary()
@Slot(dict)
def apply_filter(self, filter_data):
"""Apply filter to violation list"""
print(f"Applying filter: {filter_data}")
# Here you would filter the violations based on criteria
# For now, just update summary
self.update_summary()
@Slot(dict)
def show_violation_details(self, violation_data):
"""Show detailed view of selected violation"""
dialog = ViolationDetailDialog(violation_data, self)
dialog.exec()
def update_summary(self):
"""Update summary statistics"""
violations = self.violation_list.violations
total = len(violations)
pending = len([v for v in violations if v.get('status') == 'Pending'])
reviewed = len([v for v in violations if v.get('status') == 'Reviewed'])
closed = len([v for v in violations if v.get('status') == 'Closed'])
self.total_label.setText(f"Total: {total}")
self.pending_label.setText(f"Pending: {pending}")
self.reviewed_label.setText(f"Reviewed: {reviewed}")
self.closed_label.setText(f"Closed: {closed}")
def apply_theme(self, dark_mode=True):
"""Apply theme to the view"""
if dark_mode:
self.setStyleSheet(f"""
QWidget {{
background-color: {MaterialColors.surface};
color: {MaterialColors.text_primary};
}}
QPushButton {{
background-color: {MaterialColors.primary};
color: {MaterialColors.text_on_primary};
border: none;
border-radius: 6px;
padding: 8px 16px;
}}
QPushButton:hover {{
background-color: {MaterialColors.primary_variant};
}}
""")