477 lines
18 KiB
Python
477 lines
18 KiB
Python
"""
|
|
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;
|
|
}}
|
|
""")
|