Files
Traffic-Intersection-Monito…/qt_app_pyside1/finale/views/violations_view.py

610 lines
23 KiB
Python

"""
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};
}}
""")