FASE 1: Parts Database - Added part_categories, part_groups, parts, vehicle_parts tables - 12 categories, 190 groups with Spanish translations - API endpoints for categories, groups, parts CRUD FASE 2: Cross-References & Aftermarket - Added manufacturers, aftermarket_parts, part_cross_references tables - 24 manufacturers, quality tier system (economy/standard/premium/oem) - Part number search across OEM and aftermarket FASE 3: Exploded Diagrams - Added diagrams, vehicle_diagrams, diagram_hotspots tables - SVG viewer with zoom controls and interactive hotspots - 3 sample diagrams (brake, oil filter, suspension) FASE 4: Search & VIN Decoder - SQLite FTS5 full-text search with auto-sync triggers - NHTSA VIN decoder API integration with 30-day cache - Unified search endpoint FASE 5: Optimization & UX - API pagination (page/per_page, max 100 items) - Dark mode with localStorage persistence - Keyboard shortcuts (/, Ctrl+K, Escape, Backspace, Ctrl+D) - Breadcrumb navigation - ARIA accessibility (labels, roles, focus management) - Skip link for keyboard users Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
698 lines
29 KiB
Python
698 lines
29 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
FASE 3: Populate exploded diagrams and hotspots
|
|
This script creates FASE 3 tables and populates them with sample diagrams,
|
|
vehicle-diagram relationships, and clickable hotspots linked to parts.
|
|
"""
|
|
|
|
import sqlite3
|
|
import os
|
|
from typing import List, Dict, Optional
|
|
|
|
# Database path configuration
|
|
DB_PATH = os.path.join(os.path.dirname(__file__), '..', 'vehicle_database.db')
|
|
SCHEMA_PATH = os.path.join(os.path.dirname(__file__), '..', 'sql', 'schema.sql')
|
|
DIAGRAMS_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'dashboard', 'static', 'diagrams')
|
|
|
|
|
|
class Fase3Manager:
|
|
"""Manager for FASE 3 tables: diagrams, vehicle_diagrams, and diagram_hotspots"""
|
|
|
|
def __init__(self, db_path: str = DB_PATH):
|
|
self.db_path = db_path
|
|
self.connection = None
|
|
|
|
def connect(self):
|
|
"""Connect to the SQLite database"""
|
|
self.connection = sqlite3.connect(self.db_path)
|
|
self.connection.row_factory = sqlite3.Row
|
|
print(f"Connected to database: {self.db_path}")
|
|
|
|
def disconnect(self):
|
|
"""Close the database connection"""
|
|
if self.connection:
|
|
self.connection.close()
|
|
print("Disconnected from database")
|
|
|
|
def create_fase3_tables(self):
|
|
"""Create FASE 3 tables from schema file"""
|
|
if not os.path.exists(SCHEMA_PATH):
|
|
raise FileNotFoundError(f"Schema file not found: {SCHEMA_PATH}")
|
|
|
|
with open(SCHEMA_PATH, 'r') as f:
|
|
schema = f.read()
|
|
|
|
if self.connection:
|
|
cursor = self.connection.cursor()
|
|
cursor.executescript(schema)
|
|
self.connection.commit()
|
|
print("FASE 3 tables created successfully")
|
|
|
|
def create_diagrams_directory(self):
|
|
"""Create the diagrams directory structure"""
|
|
if not os.path.exists(DIAGRAMS_DIR):
|
|
os.makedirs(DIAGRAMS_DIR)
|
|
print(f"Created diagrams directory: {DIAGRAMS_DIR}")
|
|
else:
|
|
print(f"Diagrams directory already exists: {DIAGRAMS_DIR}")
|
|
|
|
def get_diagram_by_name(self, name: str) -> Optional[int]:
|
|
"""Get diagram ID by name, returns None if not found"""
|
|
cursor = self.connection.cursor()
|
|
cursor.execute("SELECT id FROM diagrams WHERE name = ?", (name,))
|
|
result = cursor.fetchone()
|
|
return result[0] if result else None
|
|
|
|
def insert_diagram(self, name: str, name_es: str, group_id: int, image_path: str,
|
|
thumbnail_path: str = None, svg_content: str = None,
|
|
width: int = 600, height: int = 400, display_order: int = 0,
|
|
source: str = None) -> int:
|
|
"""Insert a diagram if it doesn't exist, return its ID"""
|
|
existing_id = self.get_diagram_by_name(name)
|
|
if existing_id:
|
|
print(f" Diagram '{name}' already exists (ID: {existing_id})")
|
|
return existing_id
|
|
|
|
cursor = self.connection.cursor()
|
|
cursor.execute(
|
|
"""INSERT INTO diagrams (name, name_es, group_id, image_path, thumbnail_path,
|
|
svg_content, width, height, display_order, source)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(name, name_es, group_id, image_path, thumbnail_path, svg_content,
|
|
width, height, display_order, source)
|
|
)
|
|
self.connection.commit()
|
|
diagram_id = cursor.lastrowid
|
|
print(f" Inserted diagram: {name} (ID: {diagram_id})")
|
|
return diagram_id
|
|
|
|
def get_vehicle_diagram(self, diagram_id: int, model_year_engine_id: int) -> Optional[int]:
|
|
"""Check if a vehicle-diagram link already exists"""
|
|
cursor = self.connection.cursor()
|
|
cursor.execute(
|
|
"""SELECT id FROM vehicle_diagrams
|
|
WHERE diagram_id = ? AND model_year_engine_id = ?""",
|
|
(diagram_id, model_year_engine_id)
|
|
)
|
|
result = cursor.fetchone()
|
|
return result[0] if result else None
|
|
|
|
def insert_vehicle_diagram(self, diagram_id: int, model_year_engine_id: int,
|
|
notes: str = None) -> int:
|
|
"""Link a diagram to a vehicle configuration"""
|
|
existing_id = self.get_vehicle_diagram(diagram_id, model_year_engine_id)
|
|
if existing_id:
|
|
return existing_id
|
|
|
|
cursor = self.connection.cursor()
|
|
cursor.execute(
|
|
"""INSERT INTO vehicle_diagrams (diagram_id, model_year_engine_id, notes)
|
|
VALUES (?, ?, ?)""",
|
|
(diagram_id, model_year_engine_id, notes)
|
|
)
|
|
self.connection.commit()
|
|
return cursor.lastrowid
|
|
|
|
def get_hotspot(self, diagram_id: int, callout_number: int) -> Optional[int]:
|
|
"""Check if a hotspot already exists for this diagram and callout"""
|
|
cursor = self.connection.cursor()
|
|
cursor.execute(
|
|
"""SELECT id FROM diagram_hotspots
|
|
WHERE diagram_id = ? AND callout_number = ?""",
|
|
(diagram_id, callout_number)
|
|
)
|
|
result = cursor.fetchone()
|
|
return result[0] if result else None
|
|
|
|
def insert_hotspot(self, diagram_id: int, part_id: int = None, callout_number: int = None,
|
|
label: str = None, shape: str = 'rect', coords: str = '',
|
|
color: str = '#e74c3c') -> int:
|
|
"""Insert a hotspot for a diagram"""
|
|
if callout_number:
|
|
existing_id = self.get_hotspot(diagram_id, callout_number)
|
|
if existing_id:
|
|
return existing_id
|
|
|
|
cursor = self.connection.cursor()
|
|
cursor.execute(
|
|
"""INSERT INTO diagram_hotspots (diagram_id, part_id, callout_number, label,
|
|
shape, coords, color)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
(diagram_id, part_id, callout_number, label, shape, coords, color)
|
|
)
|
|
self.connection.commit()
|
|
return cursor.lastrowid
|
|
|
|
def get_part_by_name_pattern(self, pattern: str) -> Optional[Dict]:
|
|
"""Get a part by name pattern"""
|
|
cursor = self.connection.cursor()
|
|
cursor.execute(
|
|
"SELECT id, oem_part_number, name FROM parts WHERE name LIKE ?",
|
|
(f"%{pattern}%",)
|
|
)
|
|
result = cursor.fetchone()
|
|
return dict(result) if result else None
|
|
|
|
def get_group_by_name(self, name: str) -> Optional[int]:
|
|
"""Get group ID by name"""
|
|
cursor = self.connection.cursor()
|
|
cursor.execute("SELECT id FROM part_groups WHERE name LIKE ?", (f"%{name}%",))
|
|
result = cursor.fetchone()
|
|
return result[0] if result else None
|
|
|
|
def get_all_model_year_engines(self) -> List[int]:
|
|
"""Get all model_year_engine IDs"""
|
|
cursor = self.connection.cursor()
|
|
cursor.execute("SELECT id FROM model_year_engine")
|
|
return [row[0] for row in cursor.fetchall()]
|
|
|
|
|
|
# SVG Templates for diagrams
|
|
def generate_brake_assembly_svg() -> str:
|
|
"""Generate SVG for front brake assembly diagram"""
|
|
return '''<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg width="600" height="400" viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
|
|
<defs>
|
|
<linearGradient id="metalGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#b0b0b0"/>
|
|
<stop offset="50%" style="stop-color:#808080"/>
|
|
<stop offset="100%" style="stop-color:#606060"/>
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
<!-- Background -->
|
|
<rect width="600" height="400" fill="#f5f5f5"/>
|
|
|
|
<!-- Title -->
|
|
<text x="300" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#333">
|
|
Front Brake Assembly / Ensamble de Freno Delantero
|
|
</text>
|
|
|
|
<!-- Brake Rotor (large circle) -->
|
|
<circle cx="250" cy="200" r="120" fill="url(#metalGrad)" stroke="#333" stroke-width="3" id="rotor"/>
|
|
<circle cx="250" cy="200" r="100" fill="none" stroke="#555" stroke-width="2"/>
|
|
<circle cx="250" cy="200" r="40" fill="#444" stroke="#333" stroke-width="2"/>
|
|
<!-- Rotor ventilation slots -->
|
|
<line x1="250" y1="60" x2="250" y2="80" stroke="#666" stroke-width="3"/>
|
|
<line x1="250" y1="320" x2="250" y2="340" stroke="#666" stroke-width="3"/>
|
|
<line x1="130" y1="200" x2="150" y2="200" stroke="#666" stroke-width="3"/>
|
|
<line x1="350" y1="200" x2="370" y2="200" stroke="#666" stroke-width="3"/>
|
|
|
|
<!-- Brake Caliper -->
|
|
<rect x="320" y="140" width="80" height="120" rx="10" ry="10" fill="#c0392b" stroke="#922b21" stroke-width="3" id="caliper"/>
|
|
<rect x="330" y="155" width="60" height="35" rx="5" ry="5" fill="#e74c3c"/>
|
|
<rect x="330" y="210" width="60" height="35" rx="5" ry="5" fill="#e74c3c"/>
|
|
<!-- Caliper bolts -->
|
|
<circle cx="340" cy="150" r="6" fill="#333"/>
|
|
<circle cx="380" cy="150" r="6" fill="#333"/>
|
|
<circle cx="340" cy="250" r="6" fill="#333"/>
|
|
<circle cx="380" cy="250" r="6" fill="#333"/>
|
|
|
|
<!-- Brake Pads (visible through caliper) -->
|
|
<rect x="300" y="160" width="15" height="80" fill="#8b7355" stroke="#5d4e37" stroke-width="2" id="pad-inner"/>
|
|
<rect x="405" y="160" width="15" height="80" fill="#8b7355" stroke="#5d4e37" stroke-width="2" id="pad-outer"/>
|
|
|
|
<!-- Callout lines and numbers -->
|
|
<!-- Callout 1: Brake Rotor -->
|
|
<line x1="170" y1="120" x2="100" y2="60" stroke="#333" stroke-width="1.5"/>
|
|
<circle cx="100" cy="60" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
|
|
<text x="100" y="65" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">1</text>
|
|
|
|
<!-- Callout 2: Brake Caliper -->
|
|
<line x1="400" y1="140" x2="480" y2="80" stroke="#333" stroke-width="1.5"/>
|
|
<circle cx="480" cy="80" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
|
|
<text x="480" y="85" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">2</text>
|
|
|
|
<!-- Callout 3: Brake Pads -->
|
|
<line x1="307" y1="250" x2="250" y2="320" stroke="#333" stroke-width="1.5"/>
|
|
<circle cx="250" cy="320" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
|
|
<text x="250" y="325" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">3</text>
|
|
|
|
<!-- Legend -->
|
|
<rect x="440" y="300" width="150" height="90" fill="white" stroke="#ccc" stroke-width="1" rx="5"/>
|
|
<text x="515" y="320" text-anchor="middle" font-family="Arial" font-size="12" font-weight="bold" fill="#333">Parts / Partes</text>
|
|
<text x="450" y="340" font-family="Arial" font-size="11" fill="#333">1. Brake Rotor / Disco</text>
|
|
<text x="450" y="358" font-family="Arial" font-size="11" fill="#333">2. Brake Caliper / Caliper</text>
|
|
<text x="450" y="376" font-family="Arial" font-size="11" fill="#333">3. Brake Pads / Balatas</text>
|
|
</svg>'''
|
|
|
|
|
|
def generate_oil_filter_system_svg() -> str:
|
|
"""Generate SVG for oil filter system diagram"""
|
|
return '''<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg width="600" height="400" viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
|
|
<defs>
|
|
<linearGradient id="oilGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#8B4513"/>
|
|
<stop offset="100%" style="stop-color:#654321"/>
|
|
</linearGradient>
|
|
<linearGradient id="filterGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
<stop offset="0%" style="stop-color:#2c3e50"/>
|
|
<stop offset="50%" style="stop-color:#34495e"/>
|
|
<stop offset="100%" style="stop-color:#2c3e50"/>
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
<!-- Background -->
|
|
<rect width="600" height="400" fill="#f5f5f5"/>
|
|
|
|
<!-- Title -->
|
|
<text x="300" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#333">
|
|
Oil Filter System / Sistema de Filtro de Aceite
|
|
</text>
|
|
|
|
<!-- Engine Block (simplified) -->
|
|
<rect x="50" y="100" width="200" height="200" fill="#555" stroke="#333" stroke-width="3" rx="10"/>
|
|
<text x="150" y="200" text-anchor="middle" font-family="Arial" font-size="14" fill="#ccc">ENGINE</text>
|
|
<text x="150" y="220" text-anchor="middle" font-family="Arial" font-size="12" fill="#999">MOTOR</text>
|
|
|
|
<!-- Oil passage from engine -->
|
|
<rect x="250" y="180" width="60" height="20" fill="url(#oilGrad)"/>
|
|
<path d="M250,190 L230,190" stroke="#8B4513" stroke-width="8" fill="none"/>
|
|
|
|
<!-- Oil Filter Housing -->
|
|
<rect x="310" y="120" width="100" height="160" fill="#777" stroke="#555" stroke-width="3" rx="5"/>
|
|
|
|
<!-- Oil Filter (canister type) -->
|
|
<rect x="320" y="140" width="80" height="120" fill="url(#filterGrad)" stroke="#1a252f" stroke-width="3" rx="8" id="oil-filter"/>
|
|
<!-- Filter ridges -->
|
|
<line x1="320" y1="160" x2="400" y2="160" stroke="#1a252f" stroke-width="2"/>
|
|
<line x1="320" y1="180" x2="400" y2="180" stroke="#1a252f" stroke-width="2"/>
|
|
<line x1="320" y1="200" x2="400" y2="200" stroke="#1a252f" stroke-width="2"/>
|
|
<line x1="320" y1="220" x2="400" y2="220" stroke="#1a252f" stroke-width="2"/>
|
|
<line x1="320" y1="240" x2="400" y2="240" stroke="#1a252f" stroke-width="2"/>
|
|
<!-- Filter label area -->
|
|
<rect x="335" y="175" width="50" height="50" fill="#2980b9" rx="3"/>
|
|
<text x="360" y="195" text-anchor="middle" font-family="Arial" font-size="10" fill="white">OIL</text>
|
|
<text x="360" y="210" text-anchor="middle" font-family="Arial" font-size="10" fill="white">FILTER</text>
|
|
|
|
<!-- Oil return passage -->
|
|
<rect x="410" y="180" width="60" height="20" fill="url(#oilGrad)"/>
|
|
|
|
<!-- Oil Pan (simplified) -->
|
|
<path d="M470,170 L530,170 L550,300 L450,300 Z" fill="#666" stroke="#444" stroke-width="3"/>
|
|
<text x="500" y="250" text-anchor="middle" font-family="Arial" font-size="12" fill="#ccc">OIL PAN</text>
|
|
<text x="500" y="265" text-anchor="middle" font-family="Arial" font-size="10" fill="#999">CARTER</text>
|
|
|
|
<!-- Flow arrows -->
|
|
<polygon points="275,185 285,190 275,195" fill="#8B4513"/>
|
|
<polygon points="435,185 445,190 435,195" fill="#8B4513"/>
|
|
|
|
<!-- Callout for Oil Filter -->
|
|
<line x1="360" y1="140" x2="360" y2="70" stroke="#333" stroke-width="1.5"/>
|
|
<line x1="360" y1="70" x2="420" y2="70" stroke="#333" stroke-width="1.5"/>
|
|
<circle cx="420" cy="70" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
|
|
<text x="420" y="75" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">1</text>
|
|
|
|
<!-- Legend -->
|
|
<rect x="50" y="320" width="200" height="60" fill="white" stroke="#ccc" stroke-width="1" rx="5"/>
|
|
<text x="150" y="340" text-anchor="middle" font-family="Arial" font-size="12" font-weight="bold" fill="#333">Parts / Partes</text>
|
|
<text x="60" y="360" font-family="Arial" font-size="11" fill="#333">1. Oil Filter / Filtro de Aceite</text>
|
|
|
|
<!-- Oil flow label -->
|
|
<text x="300" y="380" text-anchor="middle" font-family="Arial" font-size="10" fill="#666">Oil Flow Direction / Direccion del Flujo de Aceite</text>
|
|
</svg>'''
|
|
|
|
|
|
def generate_suspension_diagram_svg() -> str:
|
|
"""Generate SVG for front suspension diagram"""
|
|
return '''<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg width="600" height="400" viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
|
|
<defs>
|
|
<linearGradient id="strutGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
<stop offset="0%" style="stop-color:#444"/>
|
|
<stop offset="50%" style="stop-color:#666"/>
|
|
<stop offset="100%" style="stop-color:#444"/>
|
|
</linearGradient>
|
|
<linearGradient id="springGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
<stop offset="0%" style="stop-color:#27ae60"/>
|
|
<stop offset="50%" style="stop-color:#2ecc71"/>
|
|
<stop offset="100%" style="stop-color:#27ae60"/>
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
<!-- Background -->
|
|
<rect width="600" height="400" fill="#f5f5f5"/>
|
|
|
|
<!-- Title -->
|
|
<text x="300" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#333">
|
|
Front Suspension / Suspension Delantera
|
|
</text>
|
|
|
|
<!-- Vehicle body mounting point -->
|
|
<rect x="180" y="50" width="240" height="30" fill="#555" stroke="#333" stroke-width="2"/>
|
|
<text x="300" y="70" text-anchor="middle" font-family="Arial" font-size="10" fill="#ccc">BODY / CARROCERIA</text>
|
|
|
|
<!-- Strut Mount (top) -->
|
|
<ellipse cx="300" cy="95" rx="35" ry="15" fill="#888" stroke="#555" stroke-width="2"/>
|
|
|
|
<!-- Strut Assembly -->
|
|
<rect x="285" y="95" width="30" height="150" fill="url(#strutGrad)" stroke="#333" stroke-width="2" id="strut"/>
|
|
<!-- Strut piston rod -->
|
|
<rect x="293" y="95" width="14" height="60" fill="#999" stroke="#777" stroke-width="1"/>
|
|
|
|
<!-- Coil Spring around strut -->
|
|
<path d="M275,120 Q310,130 275,140 Q240,150 275,160 Q310,170 275,180 Q240,190 275,200 Q310,210 275,220"
|
|
fill="none" stroke="url(#springGrad)" stroke-width="8" stroke-linecap="round"/>
|
|
|
|
<!-- Lower Control Arm -->
|
|
<path d="M150,320 L300,280 L450,320" fill="none" stroke="#444" stroke-width="12" stroke-linecap="round"/>
|
|
<rect x="140" y="310" width="30" height="30" fill="#666" stroke="#444" stroke-width="2" rx="5"/>
|
|
<rect x="430" y="310" width="30" height="30" fill="#666" stroke="#444" stroke-width="2" rx="5"/>
|
|
|
|
<!-- Ball Joint (connecting strut to control arm) -->
|
|
<circle cx="300" cy="280" r="20" fill="#c0392b" stroke="#922b21" stroke-width="3" id="ball-joint"/>
|
|
<circle cx="300" cy="280" r="8" fill="#333"/>
|
|
|
|
<!-- Steering Knuckle (simplified) -->
|
|
<rect x="280" y="250" width="40" height="25" fill="#777" stroke="#555" stroke-width="2"/>
|
|
|
|
<!-- Wheel hub representation -->
|
|
<circle cx="300" cy="340" r="40" fill="#444" stroke="#333" stroke-width="3"/>
|
|
<circle cx="300" cy="340" r="15" fill="#333"/>
|
|
<text x="300" y="345" text-anchor="middle" font-family="Arial" font-size="8" fill="#999">HUB</text>
|
|
|
|
<!-- Sway Bar Link -->
|
|
<line x1="350" y1="300" x2="420" y2="250" stroke="#555" stroke-width="6"/>
|
|
<circle cx="350" cy="300" r="6" fill="#777" stroke="#555" stroke-width="2"/>
|
|
<circle cx="420" cy="250" r="6" fill="#777" stroke="#555" stroke-width="2"/>
|
|
|
|
<!-- Callout 1: Strut Assembly -->
|
|
<line x1="320" y1="150" x2="420" y2="100" stroke="#333" stroke-width="1.5"/>
|
|
<circle cx="420" cy="100" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
|
|
<text x="420" y="105" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">1</text>
|
|
|
|
<!-- Callout 2: Ball Joint -->
|
|
<line x1="280" y1="280" x2="180" y2="280" stroke="#333" stroke-width="1.5"/>
|
|
<line x1="180" y1="280" x2="150" y2="250" stroke="#333" stroke-width="1.5"/>
|
|
<circle cx="150" cy="250" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
|
|
<text x="150" y="255" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">2</text>
|
|
|
|
<!-- Callout 3: Control Arm -->
|
|
<line x1="400" y1="320" x2="500" y2="350" stroke="#333" stroke-width="1.5"/>
|
|
<circle cx="500" cy="350" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
|
|
<text x="500" y="355" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">3</text>
|
|
|
|
<!-- Legend -->
|
|
<rect x="440" y="50" width="150" height="100" fill="white" stroke="#ccc" stroke-width="1" rx="5"/>
|
|
<text x="515" y="70" text-anchor="middle" font-family="Arial" font-size="12" font-weight="bold" fill="#333">Parts / Partes</text>
|
|
<text x="450" y="90" font-family="Arial" font-size="10" fill="#333">1. Strut / Amortiguador</text>
|
|
<text x="450" y="108" font-family="Arial" font-size="10" fill="#333">2. Ball Joint / Rotula</text>
|
|
<text x="450" y="126" font-family="Arial" font-size="10" fill="#333">3. Control Arm / Brazo</text>
|
|
</svg>'''
|
|
|
|
|
|
def save_svg_file(filename: str, content: str):
|
|
"""Save SVG content to file"""
|
|
filepath = os.path.join(DIAGRAMS_DIR, filename)
|
|
with open(filepath, 'w', encoding='utf-8') as f:
|
|
f.write(content)
|
|
print(f" Saved SVG file: {filepath}")
|
|
return filepath
|
|
|
|
|
|
def populate_diagrams(manager: Fase3Manager) -> Dict[str, int]:
|
|
"""Populate the diagrams table with sample diagrams"""
|
|
print("\n=== Populating Diagrams ===")
|
|
diagram_ids = {}
|
|
|
|
# Get group IDs for different diagram types
|
|
brake_rotor_group = manager.get_group_by_name("Brake Rotors")
|
|
oil_filter_group = manager.get_group_by_name("Oil Filters")
|
|
struts_group = manager.get_group_by_name("Struts")
|
|
|
|
# Use fallback group IDs if not found
|
|
if not brake_rotor_group:
|
|
brake_rotor_group = 17 # Default Brake Rotors group
|
|
if not oil_filter_group:
|
|
oil_filter_group = 70 # Default Oil Filters group
|
|
if not struts_group:
|
|
struts_group = 157 # Default Struts group
|
|
|
|
diagrams_data = [
|
|
{
|
|
'name': 'Front Brake Assembly',
|
|
'name_es': 'Ensamble de Freno Delantero',
|
|
'group_id': brake_rotor_group,
|
|
'image_path': 'diagrams/brake_assembly.svg',
|
|
'svg_generator': generate_brake_assembly_svg,
|
|
'svg_filename': 'brake_assembly.svg',
|
|
'source': 'System Generated'
|
|
},
|
|
{
|
|
'name': 'Oil Filter System',
|
|
'name_es': 'Sistema de Filtro de Aceite',
|
|
'group_id': oil_filter_group,
|
|
'image_path': 'diagrams/oil_filter_system.svg',
|
|
'svg_generator': generate_oil_filter_system_svg,
|
|
'svg_filename': 'oil_filter_system.svg',
|
|
'source': 'System Generated'
|
|
},
|
|
{
|
|
'name': 'Front Suspension Assembly',
|
|
'name_es': 'Ensamble de Suspension Delantera',
|
|
'group_id': struts_group,
|
|
'image_path': 'diagrams/suspension_assembly.svg',
|
|
'svg_generator': generate_suspension_diagram_svg,
|
|
'svg_filename': 'suspension_assembly.svg',
|
|
'source': 'System Generated'
|
|
}
|
|
]
|
|
|
|
for diagram_data in diagrams_data:
|
|
# Generate SVG content
|
|
svg_content = diagram_data['svg_generator']()
|
|
|
|
# Save SVG file
|
|
save_svg_file(diagram_data['svg_filename'], svg_content)
|
|
|
|
# Insert diagram record
|
|
diagram_id = manager.insert_diagram(
|
|
name=diagram_data['name'],
|
|
name_es=diagram_data['name_es'],
|
|
group_id=diagram_data['group_id'],
|
|
image_path=diagram_data['image_path'],
|
|
thumbnail_path=None,
|
|
svg_content=svg_content,
|
|
width=600,
|
|
height=400,
|
|
display_order=0,
|
|
source=diagram_data['source']
|
|
)
|
|
diagram_ids[diagram_data['name']] = diagram_id
|
|
|
|
print(f"\nTotal diagrams created: {len(diagram_ids)}")
|
|
return diagram_ids
|
|
|
|
|
|
def populate_vehicle_diagrams(manager: Fase3Manager, diagram_ids: Dict[str, int]):
|
|
"""Link diagrams to vehicle configurations"""
|
|
print("\n=== Linking Diagrams to Vehicles ===")
|
|
|
|
# Get all model_year_engine entries
|
|
mye_ids = manager.get_all_model_year_engines()
|
|
|
|
if not mye_ids:
|
|
print("No vehicle configurations found. Skipping vehicle-diagram links.")
|
|
return
|
|
|
|
total_links = 0
|
|
|
|
# Link each diagram to all vehicle configurations (diagrams are generic)
|
|
for diagram_name, diagram_id in diagram_ids.items():
|
|
for mye_id in mye_ids:
|
|
manager.insert_vehicle_diagram(
|
|
diagram_id=diagram_id,
|
|
model_year_engine_id=mye_id,
|
|
notes=f"Standard {diagram_name.lower()} diagram"
|
|
)
|
|
total_links += 1
|
|
|
|
print(f" Created {total_links} vehicle-diagram links")
|
|
|
|
|
|
def populate_hotspots(manager: Fase3Manager, diagram_ids: Dict[str, int]):
|
|
"""Create hotspots for each diagram linking to actual parts"""
|
|
print("\n=== Creating Diagram Hotspots ===")
|
|
|
|
total_hotspots = 0
|
|
|
|
# Hotspots for Brake Assembly diagram
|
|
if 'Front Brake Assembly' in diagram_ids:
|
|
diagram_id = diagram_ids['Front Brake Assembly']
|
|
print(f"\n Creating hotspots for Front Brake Assembly (ID: {diagram_id})")
|
|
|
|
# Find parts to link
|
|
brake_rotor = manager.get_part_by_name_pattern("Brake Rotor")
|
|
brake_pads = manager.get_part_by_name_pattern("Brake Pads")
|
|
brake_caliper = manager.get_part_by_name_pattern("Caliper")
|
|
|
|
# Hotspot 1: Brake Rotor - rectangle around the rotor area
|
|
manager.insert_hotspot(
|
|
diagram_id=diagram_id,
|
|
part_id=brake_rotor['id'] if brake_rotor else None,
|
|
callout_number=1,
|
|
label="Brake Rotor / Disco de Freno",
|
|
shape='circle',
|
|
coords='250,200,120', # cx,cy,r for circle
|
|
color='#3498db'
|
|
)
|
|
total_hotspots += 1
|
|
print(f" Added hotspot 1: Brake Rotor")
|
|
|
|
# Hotspot 2: Brake Caliper - rectangle around caliper
|
|
manager.insert_hotspot(
|
|
diagram_id=diagram_id,
|
|
part_id=brake_caliper['id'] if brake_caliper else None,
|
|
callout_number=2,
|
|
label="Brake Caliper / Calibrador",
|
|
shape='rect',
|
|
coords='320,140,80,120', # x,y,width,height for rect
|
|
color='#e74c3c'
|
|
)
|
|
total_hotspots += 1
|
|
print(f" Added hotspot 2: Brake Caliper")
|
|
|
|
# Hotspot 3: Brake Pads - rectangle around pad area
|
|
manager.insert_hotspot(
|
|
diagram_id=diagram_id,
|
|
part_id=brake_pads['id'] if brake_pads else None,
|
|
callout_number=3,
|
|
label="Brake Pads / Balatas",
|
|
shape='rect',
|
|
coords='300,160,120,80', # x,y,width,height
|
|
color='#8b7355'
|
|
)
|
|
total_hotspots += 1
|
|
print(f" Added hotspot 3: Brake Pads")
|
|
|
|
# Hotspots for Oil Filter System diagram
|
|
if 'Oil Filter System' in diagram_ids:
|
|
diagram_id = diagram_ids['Oil Filter System']
|
|
print(f"\n Creating hotspots for Oil Filter System (ID: {diagram_id})")
|
|
|
|
# Find oil filter part
|
|
oil_filter = manager.get_part_by_name_pattern("Oil Filter")
|
|
|
|
# Hotspot 1: Oil Filter
|
|
manager.insert_hotspot(
|
|
diagram_id=diagram_id,
|
|
part_id=oil_filter['id'] if oil_filter else None,
|
|
callout_number=1,
|
|
label="Oil Filter / Filtro de Aceite",
|
|
shape='rect',
|
|
coords='320,140,80,120', # x,y,width,height
|
|
color='#2980b9'
|
|
)
|
|
total_hotspots += 1
|
|
print(f" Added hotspot 1: Oil Filter")
|
|
|
|
# Hotspots for Suspension Assembly diagram
|
|
if 'Front Suspension Assembly' in diagram_ids:
|
|
diagram_id = diagram_ids['Front Suspension Assembly']
|
|
print(f"\n Creating hotspots for Front Suspension Assembly (ID: {diagram_id})")
|
|
|
|
# Find parts
|
|
strut = manager.get_part_by_name_pattern("Strut")
|
|
ball_joint = manager.get_part_by_name_pattern("Ball Joint")
|
|
control_arm = manager.get_part_by_name_pattern("Control Arm")
|
|
|
|
# Hotspot 1: Strut Assembly
|
|
manager.insert_hotspot(
|
|
diagram_id=diagram_id,
|
|
part_id=strut['id'] if strut else None,
|
|
callout_number=1,
|
|
label="Strut Assembly / Amortiguador",
|
|
shape='rect',
|
|
coords='275,95,50,150', # x,y,width,height
|
|
color='#27ae60'
|
|
)
|
|
total_hotspots += 1
|
|
print(f" Added hotspot 1: Strut Assembly")
|
|
|
|
# Hotspot 2: Ball Joint
|
|
manager.insert_hotspot(
|
|
diagram_id=diagram_id,
|
|
part_id=ball_joint['id'] if ball_joint else None,
|
|
callout_number=2,
|
|
label="Ball Joint / Rotula",
|
|
shape='circle',
|
|
coords='300,280,20', # cx,cy,r
|
|
color='#c0392b'
|
|
)
|
|
total_hotspots += 1
|
|
print(f" Added hotspot 2: Ball Joint")
|
|
|
|
# Hotspot 3: Control Arm
|
|
manager.insert_hotspot(
|
|
diagram_id=diagram_id,
|
|
part_id=control_arm['id'] if control_arm else None,
|
|
callout_number=3,
|
|
label="Control Arm / Brazo de Control",
|
|
shape='poly',
|
|
coords='150,320,300,280,450,320,430,340,300,310,170,340', # polygon points
|
|
color='#444'
|
|
)
|
|
total_hotspots += 1
|
|
print(f" Added hotspot 3: Control Arm")
|
|
|
|
print(f"\nTotal hotspots created: {total_hotspots}")
|
|
|
|
|
|
def main():
|
|
"""Main entry point for FASE 3 population"""
|
|
print("=" * 60)
|
|
print("FASE 3: Exploded Diagrams and Hotspots Population")
|
|
print("=" * 60)
|
|
|
|
manager = Fase3Manager()
|
|
|
|
try:
|
|
# Connect to database
|
|
manager.connect()
|
|
|
|
# Create FASE 3 tables (idempotent)
|
|
manager.create_fase3_tables()
|
|
|
|
# Create diagrams directory
|
|
manager.create_diagrams_directory()
|
|
|
|
# Populate diagrams
|
|
diagram_ids = populate_diagrams(manager)
|
|
|
|
# Link diagrams to vehicles
|
|
populate_vehicle_diagrams(manager, diagram_ids)
|
|
|
|
# Create hotspots
|
|
populate_hotspots(manager, diagram_ids)
|
|
|
|
print("\n" + "=" * 60)
|
|
print("FASE 3 population completed successfully!")
|
|
print("=" * 60)
|
|
|
|
# Print summary
|
|
cursor = manager.connection.cursor()
|
|
cursor.execute("SELECT COUNT(*) FROM diagrams")
|
|
diagram_count = cursor.fetchone()[0]
|
|
cursor.execute("SELECT COUNT(*) FROM vehicle_diagrams")
|
|
vd_count = cursor.fetchone()[0]
|
|
cursor.execute("SELECT COUNT(*) FROM diagram_hotspots")
|
|
hotspot_count = cursor.fetchone()[0]
|
|
|
|
print(f"\nSummary:")
|
|
print(f" Diagrams: {diagram_count}")
|
|
print(f" Vehicle-Diagram Links: {vd_count}")
|
|
print(f" Hotspots: {hotspot_count}")
|
|
print(f"\nSVG files saved to: {DIAGRAMS_DIR}")
|
|
|
|
except Exception as e:
|
|
print(f"\nError: {e}")
|
|
raise
|
|
|
|
finally:
|
|
manager.disconnect()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|