1205 lines
42 KiB
HTML
1205 lines
42 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ar" dir="rtl">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Siro Rider – محاكاة دورة حياة الرحلة</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&display=swap" rel="stylesheet" />
|
||
<style>
|
||
:root {
|
||
--bg: #0d0f1a;
|
||
--surface: #161929;
|
||
--surface2: #1e2238;
|
||
--border: #2a3050;
|
||
--primary: #5b8cff;
|
||
--amber: #f59e0b;
|
||
--green: #22c55e;
|
||
--red: #ef4444;
|
||
--blue: #3b82f6;
|
||
--purple: #a855f7;
|
||
--teal: #14b8a6;
|
||
--text: #e2e8f0;
|
||
--muted: #64748b;
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
font-family: 'Tajawal', sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
min-height: 100vh;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* ───────── Header ───────── */
|
||
.header {
|
||
background: linear-gradient(135deg, #1e2a5e 0%, #162040 100%);
|
||
padding: 20px 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 1px solid var(--border);
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
}
|
||
.header-brand { display: flex; align-items: center; gap: 12px; }
|
||
.logo-circle {
|
||
width: 44px; height: 44px;
|
||
background: linear-gradient(135deg, var(--primary), #7c3aed);
|
||
border-radius: 12px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 20px; font-weight: 900; color: #fff;
|
||
}
|
||
.header h1 { font-size: 20px; font-weight: 700; }
|
||
.header p { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
||
|
||
/* ───────── Speed control ───────── */
|
||
.speed-panel {
|
||
display: flex; align-items: center; gap: 12px;
|
||
background: var(--surface2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
padding: 8px 16px;
|
||
}
|
||
.speed-panel label { font-size: 13px; color: var(--muted); }
|
||
.speed-panel input[type=range] { width: 90px; accent-color: var(--primary); }
|
||
.speed-panel span { font-size: 14px; font-weight: 700; color: var(--primary); min-width: 36px; }
|
||
|
||
/* ───────── Main layout ───────── */
|
||
.main {
|
||
display: grid;
|
||
grid-template-columns: 340px 1fr 300px;
|
||
gap: 0;
|
||
height: calc(100vh - 75px);
|
||
}
|
||
|
||
/* ───────── Side panels ───────── */
|
||
.panel {
|
||
background: var(--surface);
|
||
border-left: 1px solid var(--border);
|
||
overflow-y: auto;
|
||
padding: 20px;
|
||
}
|
||
.panel:first-child { border-right: 1px solid var(--border); border-left: none; }
|
||
.panel-title {
|
||
font-size: 13px; font-weight: 700; text-transform: uppercase;
|
||
letter-spacing: 1px; color: var(--muted); margin-bottom: 16px;
|
||
}
|
||
|
||
/* Phase stepper */
|
||
.phase-list { display: flex; flex-direction: column; gap: 8px; }
|
||
.phase-item {
|
||
display: flex; align-items: center; gap: 12px;
|
||
padding: 12px 14px;
|
||
border-radius: 12px;
|
||
border: 1.5px solid transparent;
|
||
cursor: pointer;
|
||
transition: all .25s;
|
||
background: var(--surface2);
|
||
}
|
||
.phase-item:hover { border-color: var(--primary); }
|
||
.phase-item.active {
|
||
border-color: var(--primary);
|
||
background: rgba(91,140,255,0.12);
|
||
}
|
||
.phase-item.done { opacity: .55; }
|
||
.phase-icon {
|
||
width: 36px; height: 36px; border-radius: 50%;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 16px; flex-shrink: 0;
|
||
}
|
||
.phase-info { flex: 1; min-width: 0; }
|
||
.phase-name { font-size: 14px; font-weight: 600; }
|
||
.phase-desc { font-size: 12px; color: var(--muted); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.phase-badge {
|
||
font-size: 10px; font-weight: 700; padding: 2px 8px;
|
||
border-radius: 20px; flex-shrink: 0;
|
||
}
|
||
|
||
/* ───────── Map canvas ───────── */
|
||
.map-area {
|
||
position: relative;
|
||
background: #121826;
|
||
overflow: hidden;
|
||
}
|
||
canvas#mapCanvas {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
/* State overlay badge */
|
||
.state-overlay {
|
||
position: absolute;
|
||
top: 16px; right: 16px;
|
||
background: rgba(13,15,26,0.88);
|
||
backdrop-filter: blur(12px);
|
||
border: 1px solid var(--border);
|
||
border-radius: 16px;
|
||
padding: 12px 18px;
|
||
min-width: 200px;
|
||
}
|
||
.state-label { font-size: 11px; color: var(--muted); margin-bottom: 4px; }
|
||
.state-value { font-size: 17px; font-weight: 700; }
|
||
|
||
/* Legend */
|
||
.legend {
|
||
position: absolute;
|
||
bottom: 16px; left: 16px;
|
||
background: rgba(13,15,26,0.88);
|
||
backdrop-filter: blur(12px);
|
||
border: 1px solid var(--border);
|
||
border-radius: 16px;
|
||
padding: 14px 18px;
|
||
display: flex; flex-direction: column; gap: 8px;
|
||
}
|
||
.legend-item { display: flex; align-items: center; gap: 10px; font-size: 12px; }
|
||
.legend-line { width: 28px; height: 3px; border-radius: 2px; flex-shrink: 0; }
|
||
|
||
/* Control buttons */
|
||
.controls {
|
||
position: absolute;
|
||
bottom: 16px; right: 16px;
|
||
display: flex; flex-direction: column; gap: 10px;
|
||
}
|
||
.btn {
|
||
padding: 10px 20px; border-radius: 12px;
|
||
font-family: inherit; font-size: 14px; font-weight: 600;
|
||
cursor: pointer; border: none; transition: all .2s;
|
||
white-space: nowrap;
|
||
}
|
||
.btn-primary { background: var(--primary); color: #fff; }
|
||
.btn-primary:hover { background: #4a7bef; transform: translateY(-1px); }
|
||
.btn-amber { background: var(--amber); color: #000; }
|
||
.btn-amber:hover { background: #d97706; transform: translateY(-1px); }
|
||
.btn-green { background: var(--green); color: #fff; }
|
||
.btn-green:hover { background: #16a34a; transform: translateY(-1px); }
|
||
.btn-red { background: var(--red); color: #fff; }
|
||
.btn-red:hover { background: #dc2626; transform: translateY(-1px); }
|
||
.btn-ghost { background: var(--surface2); color: var(--text); border: 1px solid var(--border); }
|
||
.btn-ghost:hover { border-color: var(--primary); transform: translateY(-1px); }
|
||
.btn:disabled { opacity: .45; cursor: not-allowed; transform: none !important; }
|
||
|
||
/* ───────── Right log panel ───────── */
|
||
.log-panel { display: flex; flex-direction: column; gap: 6px; }
|
||
.log-entry {
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
background: var(--surface2);
|
||
border-right: 3px solid var(--border);
|
||
animation: fadeSlide .3s ease;
|
||
}
|
||
.log-entry.info { border-color: var(--primary); }
|
||
.log-entry.warn { border-color: var(--amber); }
|
||
.log-entry.ok { border-color: var(--green); }
|
||
.log-entry.err { border-color: var(--red); }
|
||
.log-time { color: var(--muted); font-size: 10px; margin-bottom: 2px; }
|
||
|
||
@keyframes fadeSlide {
|
||
from { opacity: 0; transform: translateX(8px); }
|
||
to { opacity: 1; transform: translateX(0); }
|
||
}
|
||
|
||
/* Stats row */
|
||
.stats-row {
|
||
display: grid; grid-template-columns: 1fr 1fr;
|
||
gap: 8px; margin-bottom: 14px;
|
||
}
|
||
.stat-card {
|
||
background: var(--surface2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
padding: 10px 12px;
|
||
}
|
||
.stat-label { font-size: 10px; color: var(--muted); }
|
||
.stat-value { font-size: 16px; font-weight: 700; margin-top: 2px; }
|
||
|
||
/* Progress bar */
|
||
.progress-bar-wrap { margin-bottom: 14px; }
|
||
.progress-bar-label { font-size: 12px; color: var(--muted); margin-bottom: 6px; display: flex; justify-content: space-between; }
|
||
.progress-bar { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
|
||
.progress-fill { height: 100%; border-radius: 3px; transition: width .4s ease; background: var(--primary); }
|
||
|
||
/* Notification toast */
|
||
.toast-container {
|
||
position: fixed; top: 90px; left: 50%;
|
||
transform: translateX(-50%);
|
||
z-index: 999; display: flex; flex-direction: column; gap: 8px;
|
||
pointer-events: none;
|
||
}
|
||
.toast {
|
||
background: rgba(30,34,56,0.97);
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
padding: 12px 20px;
|
||
font-size: 14px; font-weight: 500;
|
||
min-width: 280px; text-align: center;
|
||
backdrop-filter: blur(12px);
|
||
animation: toastIn .3s ease;
|
||
pointer-events: auto;
|
||
}
|
||
@keyframes toastIn {
|
||
from { opacity: 0; transform: translateY(-12px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
/* Scrollbar */
|
||
::-webkit-scrollbar { width: 4px; }
|
||
::-webkit-scrollbar-track { background: transparent; }
|
||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||
|
||
/* Divider */
|
||
.divider { height: 1px; background: var(--border); margin: 14px 0; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ───── Header ───── -->
|
||
<header class="header">
|
||
<div class="header-brand">
|
||
<div class="logo-circle">S</div>
|
||
<div>
|
||
<h1>Siro Rider — محاكاة دورة حياة الرحلة</h1>
|
||
<p>Ride Lifecycle Simulation • polyline / marker / route drawing verification</p>
|
||
</div>
|
||
</div>
|
||
<div class="speed-panel">
|
||
<label>سرعة المحاكاة</label>
|
||
<input type="range" id="simSpeed" min="0.5" max="5" step="0.5" value="1.5" oninput="updateSpeed(this.value)"/>
|
||
<span id="speedLabel">1.5×</span>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- ───── Main grid ───── -->
|
||
<div class="main">
|
||
|
||
<!-- LEFT: phases -->
|
||
<div class="panel" id="leftPanel">
|
||
<div class="panel-title">مراحل دورة الرحلة</div>
|
||
<div class="phase-list" id="phaseList"></div>
|
||
</div>
|
||
|
||
<!-- CENTER: map -->
|
||
<div class="map-area">
|
||
<canvas id="mapCanvas"></canvas>
|
||
|
||
<!-- State overlay -->
|
||
<div class="state-overlay">
|
||
<div class="state-label">الحالة الحالية</div>
|
||
<div class="state-value" id="stateLabel">noRide</div>
|
||
<div class="divider" style="margin:8px 0"></div>
|
||
<div style="font-size:12px; color:var(--muted)">المسافة للسائق</div>
|
||
<div id="etaDistance" style="font-size:15px;font-weight:700">—</div>
|
||
<div style="font-size:12px; color:var(--muted); margin-top:6px">الوقت المتبقي</div>
|
||
<div id="etaTime" style="font-size:15px;font-weight:700">—</div>
|
||
</div>
|
||
|
||
<!-- Legend -->
|
||
<div class="legend">
|
||
<div style="font-size:11px;font-weight:700;color:var(--muted);margin-bottom:4px">دليل الخطوط</div>
|
||
<div class="legend-item">
|
||
<div class="legend-line" style="background:rgba(59,130,246,0.5); border-top: 2px dashed #3b82f6"></div>
|
||
خط الرحلة الأصلي (أزرق فاتح)
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-line" style="background:#f59e0b"></div>
|
||
مسار السائق للراكب (أصفر – Apply)
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-line" style="background:#3b82f6"></div>
|
||
خط الرحلة الفعلية (أزرق – Begin)
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-line" style="border-top:2px dashed #94a3b8"></div>
|
||
خط المشي المنقط للراكب
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Controls -->
|
||
<div class="controls">
|
||
<button id="btnNext" class="btn btn-primary" onclick="nextPhase()">▶ المرحلة التالية</button>
|
||
<button id="btnAuto" class="btn btn-amber" onclick="toggleAuto()">⚡ تشغيل تلقائي</button>
|
||
<button id="btnReset" class="btn btn-ghost" onclick="resetSim()">↺ إعادة تشغيل</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RIGHT: logs -->
|
||
<div class="panel" id="rightPanel">
|
||
<div class="panel-title">سجل الأحداث</div>
|
||
|
||
<div class="stats-row">
|
||
<div class="stat-card">
|
||
<div class="stat-label">مرحلة</div>
|
||
<div class="stat-value" id="statPhase">0/6</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Polylines</div>
|
||
<div class="stat-value" id="statPolylines">0</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Markers</div>
|
||
<div class="stat-value" id="statMarkers">0</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Socket</div>
|
||
<div class="stat-value" id="statSocket" style="color:var(--red)">✕</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="progress-bar-wrap">
|
||
<div class="progress-bar-label">
|
||
<span>تقدم الرحلة</span>
|
||
<span id="progressPct">0%</span>
|
||
</div>
|
||
<div class="progress-bar"><div class="progress-fill" id="progressFill" style="width:0%"></div></div>
|
||
</div>
|
||
|
||
<div class="divider"></div>
|
||
<div class="log-panel" id="logPanel"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast container -->
|
||
<div class="toast-container" id="toastContainer"></div>
|
||
|
||
<script>
|
||
// ═══════════════════════════════════════════════════════
|
||
// SIMULATION DATA
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
// Amman-like coordinate space (normalized to canvas)
|
||
// We use a simple projection: lat/lng → pixel
|
||
const MAP = {
|
||
// bounding box (Amman)
|
||
minLat: 31.93, maxLat: 32.10,
|
||
minLng: 35.82, maxLng: 35.98,
|
||
};
|
||
|
||
// Key positions (lat, lng)
|
||
const PASSENGER_POS = { lat: 32.02, lng: 35.88 }; // نقطة التقاط الراكب
|
||
const DESTINATION = { lat: 31.97, lng: 35.93 }; // وجهة الراكب
|
||
const DRIVER_START = { lat: 32.06, lng: 35.85 }; // موقع السائق البداية
|
||
|
||
// Route from driver to passenger (simulated waypoints)
|
||
const DRIVER_TO_PASSENGER_ROUTE = [
|
||
{ lat: 32.060, lng: 35.850 },
|
||
{ lat: 32.055, lng: 35.856 },
|
||
{ lat: 32.049, lng: 35.862 },
|
||
{ lat: 32.043, lng: 35.868 },
|
||
{ lat: 32.036, lng: 35.872 },
|
||
{ lat: 32.029, lng: 35.876 },
|
||
{ lat: 32.023, lng: 35.879 },
|
||
{ lat: 32.020, lng: 35.880 }, // ← passenger pickup
|
||
];
|
||
|
||
// Route from passenger to destination
|
||
const TRIP_ROUTE = [
|
||
{ lat: 32.020, lng: 35.880 },
|
||
{ lat: 32.013, lng: 35.884 },
|
||
{ lat: 32.005, lng: 35.888 },
|
||
{ lat: 31.998, lng: 35.893 },
|
||
{ lat: 31.992, lng: 35.898 },
|
||
{ lat: 31.985, lng: 35.913 },
|
||
{ lat: 31.980, lng: 35.922 },
|
||
{ lat: 31.975, lng: 35.928 },
|
||
{ lat: 31.970, lng: 35.930 }, // ← destination
|
||
];
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// STATE
|
||
// ═══════════════════════════════════════════════════════
|
||
let canvas, ctx;
|
||
let simState = {
|
||
phase: -1,
|
||
driverPos: { ...DRIVER_START },
|
||
carHeading: 135,
|
||
driverRouteIdx: 0,
|
||
tripRouteIdx: 0,
|
||
polylines: [], // { id, points, color, width, dashed }
|
||
markers: [], // { id, pos, type, rotation }
|
||
isAutoRunning: false,
|
||
autoTimer: null,
|
||
animFrame: null,
|
||
socketConnected: false,
|
||
etaSeconds: 0,
|
||
distanceMeters: 0,
|
||
driverEtaTimer: null,
|
||
driverMoveTimer: null,
|
||
simSpeed: 1.5,
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// PHASES DEFINITION
|
||
// ═══════════════════════════════════════════════════════
|
||
const PHASES = [
|
||
{
|
||
id: 'noRide',
|
||
name: 'noRide — لا رحلة',
|
||
desc: 'الحالة الافتراضية، عرض السيارات القريبة',
|
||
color: '#64748b',
|
||
icon: '🗺️',
|
||
badge: 'IDLE',
|
||
badgeColor: '#64748b',
|
||
enter: enterNoRide,
|
||
},
|
||
{
|
||
id: 'searching',
|
||
name: 'searching — البحث عن سائق',
|
||
desc: 'الراكب طلب رحلة، النظام يبحث',
|
||
color: '#a855f7',
|
||
icon: '🔍',
|
||
badge: 'SEARCH',
|
||
badgeColor: '#a855f7',
|
||
enter: enterSearching,
|
||
},
|
||
{
|
||
id: 'driverApplied',
|
||
name: 'driverApplied — السائق قبل',
|
||
desc: 'رسم مسار السائق (أصفر) + خط منقط للراكب',
|
||
color: '#f59e0b',
|
||
icon: '🚕',
|
||
badge: 'APPLY',
|
||
badgeColor: '#f59e0b',
|
||
enter: enterDriverApplied,
|
||
},
|
||
{
|
||
id: 'driverMoving',
|
||
name: 'السائق يتحرك نحو الراكب',
|
||
desc: 'تحديث real-time للمسار المتبقي',
|
||
color: '#f59e0b',
|
||
icon: '📡',
|
||
badge: 'MOVING',
|
||
badgeColor: '#3b82f6',
|
||
enter: enterDriverMoving,
|
||
},
|
||
{
|
||
id: 'driverArrived',
|
||
name: 'driverArrived — وصل السائق',
|
||
desc: 'مسح الخطوط القديمة، رسم مسار الرحلة',
|
||
color: '#22c55e',
|
||
icon: '📍',
|
||
badge: 'ARRIVED',
|
||
badgeColor: '#22c55e',
|
||
enter: enterDriverArrived,
|
||
},
|
||
{
|
||
id: 'inProgress',
|
||
name: 'inProgress — الرحلة بدأت',
|
||
desc: 'خط أزرق من موقع السائق للوجهة',
|
||
color: '#3b82f6',
|
||
icon: '🚗',
|
||
badge: 'BEGIN',
|
||
badgeColor: '#3b82f6',
|
||
enter: enterInProgress,
|
||
},
|
||
{
|
||
id: 'tripMoving',
|
||
name: 'السيارة تسير نحو الوجهة',
|
||
desc: 'تحديث المسار المتبقي أثناء السفر',
|
||
color: '#3b82f6',
|
||
icon: '🏎️',
|
||
badge: 'EN ROUTE',
|
||
badgeColor: '#14b8a6',
|
||
enter: enterTripMoving,
|
||
},
|
||
{
|
||
id: 'finished',
|
||
name: 'finished — انتهت الرحلة',
|
||
desc: 'مسح الخريطة، فتح شاشة التقييم',
|
||
color: '#22c55e',
|
||
icon: '⭐',
|
||
badge: 'DONE',
|
||
badgeColor: '#22c55e',
|
||
enter: enterFinished,
|
||
},
|
||
];
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// CANVAS helpers
|
||
// ═══════════════════════════════════════════════════════
|
||
function toPixel(lat, lng) {
|
||
const x = ((lng - MAP.minLng) / (MAP.maxLng - MAP.minLng)) * canvas.width;
|
||
const y = canvas.height - ((lat - MAP.minLat) / (MAP.maxLat - MAP.minLat)) * canvas.height;
|
||
return { x, y };
|
||
}
|
||
|
||
function drawMapBackground() {
|
||
// Dark map bg
|
||
ctx.fillStyle = '#0e1420';
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// Grid lines (streets feel)
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.04)';
|
||
ctx.lineWidth = 1;
|
||
for (let x = 0; x < canvas.width; x += 60) {
|
||
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
|
||
}
|
||
for (let y = 0; y < canvas.height; y += 60) {
|
||
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
|
||
}
|
||
|
||
// Diagonal "streets"
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.06)';
|
||
ctx.lineWidth = 2;
|
||
const streets = [
|
||
[{ lat: 32.08, lng: 35.82 }, { lat: 31.93, lng: 35.97 }],
|
||
[{ lat: 32.09, lng: 35.87 }, { lat: 31.95, lng: 35.98 }],
|
||
[{ lat: 32.06, lng: 35.82 }, { lat: 31.94, lng: 35.96 }],
|
||
[{ lat: 32.03, lng: 35.82 }, { lat: 31.93, lng: 35.93 }],
|
||
];
|
||
streets.forEach(seg => {
|
||
const a = toPixel(seg[0].lat, seg[0].lng);
|
||
const b = toPixel(seg[1].lat, seg[1].lng);
|
||
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
|
||
});
|
||
|
||
// Compass
|
||
ctx.fillStyle = 'rgba(100,116,139,0.6)';
|
||
ctx.font = '14px Tajawal';
|
||
ctx.fillText('N ↑', 16, 24);
|
||
}
|
||
|
||
function drawPolylines() {
|
||
simState.polylines.forEach(poly => {
|
||
if (!poly.points || poly.points.length < 2) return;
|
||
ctx.save();
|
||
ctx.strokeStyle = poly.color || '#3b82f6';
|
||
ctx.lineWidth = poly.width || 4;
|
||
ctx.lineCap = 'round';
|
||
ctx.lineJoin = 'round';
|
||
|
||
if (poly.dashed) {
|
||
ctx.setLineDash([12, 8]);
|
||
} else {
|
||
ctx.setLineDash([]);
|
||
}
|
||
|
||
// Glow effect for main routes
|
||
if (!poly.dashed && poly.width >= 4) {
|
||
ctx.shadowColor = poly.color;
|
||
ctx.shadowBlur = 10;
|
||
}
|
||
|
||
ctx.beginPath();
|
||
const first = toPixel(poly.points[0].lat, poly.points[0].lng);
|
||
ctx.moveTo(first.x, first.y);
|
||
for (let i = 1; i < poly.points.length; i++) {
|
||
const p = toPixel(poly.points[i].lat, poly.points[i].lng);
|
||
ctx.lineTo(p.x, p.y);
|
||
}
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
});
|
||
}
|
||
|
||
function drawMarkers() {
|
||
simState.markers.forEach(m => {
|
||
const p = toPixel(m.pos.lat, m.pos.lng);
|
||
ctx.save();
|
||
ctx.translate(p.x, p.y);
|
||
|
||
if (m.type === 'car') {
|
||
// Rotate car
|
||
ctx.rotate((m.rotation || 0) * Math.PI / 180);
|
||
// Shadow
|
||
ctx.shadowColor = '#f59e0b';
|
||
ctx.shadowBlur = 16;
|
||
// Car body
|
||
ctx.fillStyle = m.inTrip ? '#3b82f6' : '#f59e0b';
|
||
roundRect(ctx, -12, -20, 24, 36, 6);
|
||
ctx.fill();
|
||
// Windshield
|
||
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
||
roundRect(ctx, -8, -16, 16, 10, 2);
|
||
ctx.fill();
|
||
// Wheels
|
||
ctx.fillStyle = '#1e293b';
|
||
[-10, 10].forEach(xw => {
|
||
[-14, 12].forEach(yw => {
|
||
ctx.fillRect(xw - 4, yw - 4, 8, 8);
|
||
});
|
||
});
|
||
// Arrow
|
||
ctx.fillStyle = '#fff';
|
||
ctx.font = 'bold 10px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText('▲', 0, -24);
|
||
} else if (m.type === 'passenger') {
|
||
// Passenger pin (orange)
|
||
ctx.shadowColor = '#f97316';
|
||
ctx.shadowBlur = 14;
|
||
ctx.fillStyle = '#f97316';
|
||
ctx.beginPath();
|
||
ctx.arc(0, 0, 10, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.fillStyle = '#fff';
|
||
ctx.font = '12px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText('🧍', 0, 0);
|
||
} else if (m.type === 'destination') {
|
||
// Destination pin (violet)
|
||
ctx.shadowColor = '#a855f7';
|
||
ctx.shadowBlur = 14;
|
||
ctx.fillStyle = '#a855f7';
|
||
ctx.beginPath();
|
||
ctx.moveTo(0, 0); ctx.lineTo(-10, -20); ctx.lineTo(10, -20);
|
||
ctx.closePath(); ctx.fill();
|
||
ctx.beginPath();
|
||
ctx.arc(0, -20, 10, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.fillStyle = '#fff';
|
||
ctx.font = '10px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText('B', 0, -20);
|
||
} else if (m.type === 'start') {
|
||
ctx.shadowColor = '#22c55e';
|
||
ctx.shadowBlur = 14;
|
||
ctx.fillStyle = '#22c55e';
|
||
ctx.beginPath();
|
||
ctx.moveTo(0, 0); ctx.lineTo(-10, -20); ctx.lineTo(10, -20);
|
||
ctx.closePath(); ctx.fill();
|
||
ctx.beginPath();
|
||
ctx.arc(0, -20, 10, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.fillStyle = '#fff';
|
||
ctx.font = '10px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText('A', 0, -20);
|
||
} else if (m.type === 'walking') {
|
||
ctx.shadowColor = '#94a3b8';
|
||
ctx.shadowBlur = 8;
|
||
ctx.fillStyle = '#94a3b8';
|
||
ctx.beginPath();
|
||
ctx.arc(0, 0, 7, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.fillStyle = '#fff';
|
||
ctx.font = '8px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText('🚶', 0, 0);
|
||
} else if (m.type === 'nearbyCar') {
|
||
ctx.globalAlpha = 0.5;
|
||
ctx.fillStyle = '#64748b';
|
||
ctx.beginPath();
|
||
ctx.arc(0, 0, 7, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.font = '9px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText('🚗', 0, 0);
|
||
}
|
||
|
||
ctx.restore();
|
||
ctx.restore();
|
||
});
|
||
}
|
||
|
||
function roundRect(ctx, x, y, w, h, r) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(x + r, y);
|
||
ctx.lineTo(x + w - r, y);
|
||
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
||
ctx.lineTo(x + w, y + h - r);
|
||
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
||
ctx.lineTo(x + r, y + h);
|
||
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
||
ctx.lineTo(x, y + r);
|
||
ctx.quadraticCurveTo(x, y, x + r, y);
|
||
ctx.closePath();
|
||
}
|
||
|
||
function render() {
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
drawMapBackground();
|
||
drawPolylines();
|
||
drawMarkers();
|
||
simState.animFrame = requestAnimationFrame(render);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// PHASE IMPLEMENTATIONS
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
function enterNoRide() {
|
||
clearMap();
|
||
log('🗺️ حالة noRide: عرض الخريطة الفارغة مع السيارات القريبة', 'info');
|
||
|
||
// Add nearby dummy cars
|
||
const nearbyCars = [
|
||
{ lat: 32.04, lng: 35.87 },
|
||
{ lat: 32.03, lng: 35.90 },
|
||
{ lat: 32.05, lng: 35.86 },
|
||
{ lat: 32.035, lng: 35.875 },
|
||
];
|
||
nearbyCars.forEach((pos, i) => {
|
||
addMarker(`nearbyCar_${i}`, pos, 'nearbyCar', 0);
|
||
});
|
||
|
||
// Passenger location marker
|
||
addMarker('passenger', PASSENGER_POS, 'passenger', 0);
|
||
|
||
setStateLabel('noRide', '#64748b');
|
||
updateEtaDisplay('—', '—');
|
||
log('✅ Master Timer يعمل كل 6 ثوانٍ لجلب السيارات القريبة', 'ok');
|
||
showToast('🗺️ الحالة: noRide — في انتظار الراكب لطلب رحلة', '#64748b');
|
||
}
|
||
|
||
function enterSearching() {
|
||
clearMap();
|
||
log('🔍 حالة searching: الراكب طلب الرحلة', 'info');
|
||
|
||
// Draw original route (faint blue)
|
||
addPolyline('trip_preview', TRIP_ROUTE, 'rgba(59,130,246,0.35)', 4, false);
|
||
|
||
// Markers: start + destination
|
||
addMarker('start', PASSENGER_POS, 'start', 0);
|
||
addMarker('destination', DESTINATION, 'destination', 0);
|
||
|
||
setStateLabel('searching', '#a855f7');
|
||
updateEtaDisplay('~2.1 كم', '~8 دق');
|
||
log('📡 postRideDetailsToServer() → رفع بيانات الرحلة', 'info');
|
||
log('🔌 initConnectionWithSocket() → فتح WebSocket', 'info');
|
||
log('⏱️ Master Timer: كل 8 ثوانٍ يتحقق من getRideStatus()', 'ok');
|
||
log('🔍 النظام يبحث عن سائق في دائرة 2400م → 3000م → 3100م', 'ok');
|
||
showToast('🔍 البحث عن سائق...', '#a855f7');
|
||
}
|
||
|
||
function enterDriverApplied() {
|
||
clearMap();
|
||
log('🚕 حالة driverApplied: السائق قبل الرحلة', 'info');
|
||
log('▶ processRideAcceptance() تم استدعاؤها', 'ok');
|
||
log('📥 getUpdatedRideForDriverApply() → بيانات السائق', 'info');
|
||
|
||
// Trip preview (faint)
|
||
addPolyline('trip_preview', TRIP_ROUTE, 'rgba(59,130,246,0.25)', 3, false);
|
||
|
||
// Driver to passenger route (AMBER solid)
|
||
addPolyline('driver_route_solid', DRIVER_TO_PASSENGER_ROUTE, '#f59e0b', 5, false);
|
||
|
||
// Walk dotted line from last road point to exact passenger pos
|
||
const lastRoadPt = DRIVER_TO_PASSENGER_ROUTE[DRIVER_TO_PASSENGER_ROUTE.length - 1];
|
||
addPolyline('passenger_walk_line', [lastRoadPt, PASSENGER_POS], '#94a3b8', 3, true);
|
||
|
||
// Markers
|
||
addMarker('car', DRIVER_START, 'car', 135);
|
||
addMarker('start', PASSENGER_POS, 'start', 0);
|
||
addMarker('destination', DESTINATION, 'destination', 0);
|
||
addMarker('walking', lastRoadPt, 'walking', 0);
|
||
|
||
simState.socketConnected = true;
|
||
updateSocketStatus(true);
|
||
|
||
setStateLabel('driverApplied', '#f59e0b');
|
||
updateEtaDisplay('~3.2 كم', '~12 دق');
|
||
|
||
log('🟡 خط أصفر صلب: مسار السائق → نقطة الالتقاط', 'ok');
|
||
log('⚪ خط منقط: آخر نقطة طريق → موقع الراكب الدقيق', 'ok');
|
||
log('🚶 walk_end_marker (أيقونة مشي) عند آخر نقطة طريق', 'ok');
|
||
log('📡 subscribe_driver_location emitted via Socket', 'ok');
|
||
log('⏱️ startTimerFromDriverToPassengerAfterApplied() يعمل', 'info');
|
||
log('⏱️ _startSocketWatchdog() كل 5 ثوانٍ', 'info');
|
||
showToast('🚕 السائق قبل رحلتك! في طريقه إليك', '#f59e0b');
|
||
}
|
||
|
||
function enterDriverMoving() {
|
||
log('📡 السائق يتحرك — تحديثات real-time', 'info');
|
||
|
||
let idx = 0;
|
||
const totalSteps = DRIVER_TO_PASSENGER_ROUTE.length;
|
||
|
||
function step() {
|
||
if (idx >= totalSteps - 1) {
|
||
log('✅ السائق وصل للمنطقة — جاهز للمرحلة التالية', 'ok');
|
||
return;
|
||
}
|
||
idx++;
|
||
const newPos = DRIVER_TO_PASSENGER_ROUTE[idx];
|
||
|
||
// Update car marker position
|
||
simState.markers = simState.markers.filter(m => m.id !== 'car');
|
||
addMarker('car', newPos, 'car', computeHeading(
|
||
DRIVER_TO_PASSENGER_ROUTE[idx - 1], newPos
|
||
));
|
||
|
||
// Trim the driver route (remaining only)
|
||
const remaining = DRIVER_TO_PASSENGER_ROUTE.slice(idx);
|
||
simState.polylines = simState.polylines.filter(p => p.id !== 'driver_route_solid');
|
||
if (remaining.length >= 2) {
|
||
addPolyline('driver_route_solid', remaining, '#f59e0b', 5, false);
|
||
}
|
||
|
||
// Update walk dotted line
|
||
const lastRoadPt = remaining.length > 0 ? remaining[remaining.length - 1] : newPos;
|
||
simState.polylines = simState.polylines.filter(p => !p.id.startsWith('passenger_walk'));
|
||
addPolyline('passenger_walk_line', [lastRoadPt, PASSENGER_POS], '#94a3b8', 3, true);
|
||
|
||
// Recalculate ETA
|
||
const remMeters = idx * 400;
|
||
const etaSec = Math.max(0, 720 - idx * 90);
|
||
const etaMin = Math.floor(etaSec / 60);
|
||
const etaSec2 = etaSec % 60;
|
||
updateEtaDisplay(`~${(totalSteps - idx) * 0.4} كم`, `${etaMin}:${String(etaSec2).padStart(2,'0')}`);
|
||
|
||
if (idx % 3 === 0) {
|
||
log(`📍 تحديث موقع السائق [${idx}/${totalSteps}] — updateRemainingRoute()`, 'info');
|
||
}
|
||
|
||
const delay = 600 / simState.simSpeed;
|
||
simState.driverMoveTimer = setTimeout(step, delay);
|
||
}
|
||
|
||
step();
|
||
showToast('📡 السائق يتحرك — تحديث real-time كل 5 ثوانٍ', '#3b82f6');
|
||
}
|
||
|
||
function enterDriverArrived() {
|
||
if (simState.driverMoveTimer) clearTimeout(simState.driverMoveTimer);
|
||
|
||
clearMap();
|
||
log('📍 حالة driverArrived: السائق وصل', 'warn');
|
||
log('🗑️ حذف خط driver_route_solid القديم', 'warn');
|
||
log('🗑️ حذف خط passenger_walk_line المنقط', 'warn');
|
||
log('✅ processDriverArrival() تم استدعاؤها', 'ok');
|
||
|
||
// Now show the TRIP route (blue solid)
|
||
addPolyline('main_route', TRIP_ROUTE, '#3b82f6', 6, false);
|
||
|
||
// Driver is now at pickup point
|
||
addMarker('car', PASSENGER_POS, 'car', 0);
|
||
addMarker('start', PASSENGER_POS, 'start', 0);
|
||
addMarker('destination', DESTINATION, 'destination', 0);
|
||
|
||
setStateLabel('driverArrived', '#22c55e');
|
||
updateEtaDisplay('وصل!', '0:00');
|
||
log('🟦 رسم main_route الأزرق من نقطة الالتقاط للوجهة', 'ok');
|
||
log('⏱️ startTimerDriverWaitPassenger5Minute() — 5 دقائق انتظار', 'info');
|
||
log('🔔 إشعار Firebase: السائق وصل!', 'ok');
|
||
showToast('📍 السائق وصل! ترقب السيارة أمامك', '#22c55e');
|
||
}
|
||
|
||
function enterInProgress() {
|
||
clearMap();
|
||
log('🚗 حالة inProgress: بدأت الرحلة!', 'ok');
|
||
log('🗑️ مسح خطوط driver_route وmain_route القديمة', 'warn');
|
||
log('▶ processRideBegin() — isBeginPhase: true', 'ok');
|
||
|
||
// Draw trip route (blue, with glow)
|
||
addPolyline('main_route', TRIP_ROUTE, '#3b82f6', 6, false);
|
||
|
||
// Car at pickup
|
||
addMarker('car', PASSENGER_POS, 'car', 315, true);
|
||
addMarker('destination', DESTINATION, 'destination', 0);
|
||
|
||
// No pickup marker — we're in trip now
|
||
simState.socketConnected = true;
|
||
updateSocketStatus(true);
|
||
|
||
setStateLabel('inProgress', '#3b82f6');
|
||
updateEtaDisplay('~4.8 كم', '~22 دق');
|
||
|
||
log('🟦 خط أزرق صلب: من موقع الراكب/السائق إلى الوجهة', 'ok');
|
||
log('⏱️ rideIsBeginPassengerTimer() — عداد الرحلة يعمل', 'info');
|
||
log('📊 runWhenRideIsBegin() — polling كل 4 ثوانٍ', 'info');
|
||
log('🗺️ calculateDriverToPassengerRoute(driverPos, destination, isBeginPhase: true)', 'ok');
|
||
showToast('🚗 الرحلة بدأت! في طريقك للوجهة', '#3b82f6');
|
||
}
|
||
|
||
function enterTripMoving() {
|
||
log('🏎️ السيارة تسير نحو الوجهة', 'info');
|
||
|
||
let idx = 0;
|
||
const total = TRIP_ROUTE.length;
|
||
|
||
function step() {
|
||
if (idx >= total - 1) {
|
||
log('✅ وصلنا للوجهة تقريباً', 'ok');
|
||
return;
|
||
}
|
||
idx++;
|
||
const newPos = TRIP_ROUTE[idx];
|
||
|
||
// Move car
|
||
simState.markers = simState.markers.filter(m => m.id !== 'car');
|
||
addMarker('car', newPos, 'car', computeHeading(TRIP_ROUTE[idx-1], newPos), true);
|
||
|
||
// Trim route (remaining)
|
||
const remaining = TRIP_ROUTE.slice(idx);
|
||
simState.polylines = simState.polylines.filter(p => p.id !== 'main_route');
|
||
if (remaining.length >= 2) {
|
||
addPolyline('main_route', remaining, '#3b82f6', 6, false);
|
||
}
|
||
|
||
// Progress
|
||
const pct = Math.round((idx / (total - 1)) * 100);
|
||
setProgress(pct);
|
||
updateEtaDisplay(
|
||
`~${((total - 1 - idx) * 0.6).toFixed(1)} كم`,
|
||
`${Math.floor((total - 1 - idx) * 2.5)}:00`
|
||
);
|
||
|
||
if (idx % 2 === 0) {
|
||
log(`🗺️ updateRemainingRoute() — قص نقاط المسار المكتملة [${pct}%]`, 'info');
|
||
}
|
||
|
||
const delay = 700 / simState.simSpeed;
|
||
simState.driverMoveTimer = setTimeout(step, delay);
|
||
}
|
||
|
||
step();
|
||
showToast('🏎️ السيارة في الطريق — المسار يتقلص تدريجياً', '#14b8a6');
|
||
}
|
||
|
||
function enterFinished() {
|
||
if (simState.driverMoveTimer) clearTimeout(simState.driverMoveTimer);
|
||
|
||
clearMap();
|
||
log('🏁 حالة finished: انتهت الرحلة', 'ok');
|
||
log('🗑️ mapEngine.clearPolyline() — مسح جميع الخطوط', 'warn');
|
||
log('🗑️ markers = {} — مسح جميع الـ markers', 'warn');
|
||
log('🔌 disposeRideSocket() — إغلاق WebSocket', 'warn');
|
||
log('⭐ فتح شاشة RatingDriverBottomSheet', 'ok');
|
||
log('🏁 stopAllTimers() — إيقاف كل التايمرات', 'warn');
|
||
|
||
simState.socketConnected = false;
|
||
updateSocketStatus(false);
|
||
|
||
// Show rating animation
|
||
const destPx = toPixel(DESTINATION.lat, DESTINATION.lng);
|
||
addMarker('destination', DESTINATION, 'destination', 0);
|
||
|
||
setStateLabel('finished', '#22c55e');
|
||
setProgress(100);
|
||
updateEtaDisplay('وصلت!', 'انتهت الرحلة');
|
||
|
||
showToast('⭐ رحلة ممتازة! شكراً لاستخدامك Siro', '#22c55e');
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// HELPER FUNCTIONS
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
function clearMap() {
|
||
simState.polylines = [];
|
||
simState.markers = [];
|
||
if (simState.driverMoveTimer) clearTimeout(simState.driverMoveTimer);
|
||
}
|
||
|
||
function addPolyline(id, points, color, width, dashed) {
|
||
simState.polylines = simState.polylines.filter(p => p.id !== id);
|
||
simState.polylines.push({ id, points: points.map(p => ({ lat: p.lat, lng: p.lng })), color, width, dashed });
|
||
updatePolylineCount();
|
||
}
|
||
|
||
function addMarker(id, pos, type, rotation, inTrip = false) {
|
||
simState.markers = simState.markers.filter(m => m.id !== id);
|
||
simState.markers.push({ id, pos: { lat: pos.lat, lng: pos.lng }, type, rotation, inTrip });
|
||
updateMarkerCount();
|
||
}
|
||
|
||
function updatePolylineCount() {
|
||
document.getElementById('statPolylines').textContent = simState.polylines.length;
|
||
}
|
||
function updateMarkerCount() {
|
||
document.getElementById('statMarkers').textContent = simState.markers.length;
|
||
}
|
||
|
||
function computeHeading(from, to) {
|
||
const dLng = to.lng - from.lng;
|
||
const dLat = to.lat - from.lat;
|
||
const angle = Math.atan2(dLng, dLat) * 180 / Math.PI;
|
||
return (angle + 360) % 360;
|
||
}
|
||
|
||
function setStateLabel(text, color) {
|
||
const el = document.getElementById('stateLabel');
|
||
el.textContent = text;
|
||
el.style.color = color;
|
||
}
|
||
|
||
function updateEtaDisplay(distance, time) {
|
||
document.getElementById('etaDistance').textContent = distance;
|
||
document.getElementById('etaTime').textContent = time;
|
||
}
|
||
|
||
function setProgress(pct) {
|
||
document.getElementById('progressFill').style.width = pct + '%';
|
||
document.getElementById('progressPct').textContent = pct + '%';
|
||
}
|
||
|
||
function updateSocketStatus(connected) {
|
||
const el = document.getElementById('statSocket');
|
||
el.textContent = connected ? '✓' : '✕';
|
||
el.style.color = connected ? 'var(--green)' : 'var(--red)';
|
||
}
|
||
|
||
function log(message, type = 'info') {
|
||
const panel = document.getElementById('logPanel');
|
||
const now = new Date();
|
||
const time = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`;
|
||
|
||
const entry = document.createElement('div');
|
||
entry.className = `log-entry ${type}`;
|
||
entry.innerHTML = `<div class="log-time">${time}</div>${message}`;
|
||
panel.insertBefore(entry, panel.firstChild);
|
||
|
||
// Limit logs
|
||
while (panel.children.length > 40) {
|
||
panel.removeChild(panel.lastChild);
|
||
}
|
||
}
|
||
|
||
function showToast(message, color = '#5b8cff') {
|
||
const container = document.getElementById('toastContainer');
|
||
const toast = document.createElement('div');
|
||
toast.className = 'toast';
|
||
toast.style.borderColor = color;
|
||
toast.style.color = color;
|
||
toast.textContent = message;
|
||
container.appendChild(toast);
|
||
setTimeout(() => {
|
||
toast.style.opacity = '0';
|
||
toast.style.transform = 'translateY(-8px)';
|
||
toast.style.transition = 'all .4s ease';
|
||
setTimeout(() => toast.remove(), 400);
|
||
}, 3000);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// PHASE NAVIGATION
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
function renderPhaseList() {
|
||
const list = document.getElementById('phaseList');
|
||
list.innerHTML = '';
|
||
PHASES.forEach((phase, i) => {
|
||
const item = document.createElement('div');
|
||
item.className = 'phase-item' + (i === simState.phase ? ' active' : '') + (i < simState.phase ? ' done' : '');
|
||
item.id = `phase_${i}`;
|
||
item.innerHTML = `
|
||
<div class="phase-icon" style="background:${phase.color}22;color:${phase.color}">${phase.icon}</div>
|
||
<div class="phase-info">
|
||
<div class="phase-name">${phase.name}</div>
|
||
<div class="phase-desc">${phase.desc}</div>
|
||
</div>
|
||
<div class="phase-badge" style="background:${phase.badgeColor}22;color:${phase.badgeColor}">${phase.badge}</div>
|
||
`;
|
||
item.onclick = () => jumpToPhase(i);
|
||
list.appendChild(item);
|
||
});
|
||
}
|
||
|
||
function jumpToPhase(idx) {
|
||
if (simState.isAutoRunning) toggleAuto();
|
||
simState.phase = idx;
|
||
executePhase(idx);
|
||
renderPhaseList();
|
||
document.getElementById('statPhase').textContent = `${idx + 1}/${PHASES.length}`;
|
||
}
|
||
|
||
function executePhase(idx) {
|
||
if (idx < 0 || idx >= PHASES.length) return;
|
||
PHASES[idx].enter();
|
||
}
|
||
|
||
function nextPhase() {
|
||
if (simState.phase >= PHASES.length - 1) {
|
||
showToast('✅ اكتملت المحاكاة!', '#22c55e');
|
||
return;
|
||
}
|
||
simState.phase++;
|
||
executePhase(simState.phase);
|
||
renderPhaseList();
|
||
document.getElementById('statPhase').textContent = `${simState.phase + 1}/${PHASES.length}`;
|
||
document.getElementById('btnNext').disabled = simState.phase >= PHASES.length - 1;
|
||
}
|
||
|
||
function toggleAuto() {
|
||
if (simState.isAutoRunning) {
|
||
// Stop
|
||
clearInterval(simState.autoTimer);
|
||
simState.autoTimer = null;
|
||
simState.isAutoRunning = false;
|
||
document.getElementById('btnAuto').textContent = '⚡ تشغيل تلقائي';
|
||
document.getElementById('btnAuto').className = 'btn btn-amber';
|
||
} else {
|
||
// Start
|
||
simState.isAutoRunning = true;
|
||
document.getElementById('btnAuto').textContent = '⏸ إيقاف مؤقت';
|
||
document.getElementById('btnAuto').className = 'btn btn-red';
|
||
|
||
const interval = () => {
|
||
if (simState.phase >= PHASES.length - 1) {
|
||
toggleAuto();
|
||
return;
|
||
}
|
||
nextPhase();
|
||
// Dynamic delay based on phase
|
||
const delay = [2000, 2500, 2000, 5000, 2000, 2000, 6000, 2000][simState.phase] / simState.simSpeed;
|
||
simState.autoTimer = setTimeout(interval, delay);
|
||
};
|
||
|
||
const firstDelay = 1500 / simState.simSpeed;
|
||
simState.autoTimer = setTimeout(interval, firstDelay);
|
||
}
|
||
}
|
||
|
||
function resetSim() {
|
||
if (simState.isAutoRunning) toggleAuto();
|
||
clearMap();
|
||
if (simState.driverMoveTimer) clearTimeout(simState.driverMoveTimer);
|
||
simState.phase = -1;
|
||
simState.socketConnected = false;
|
||
updateSocketStatus(false);
|
||
setStateLabel('noRide', '#64748b');
|
||
updateEtaDisplay('—', '—');
|
||
setProgress(0);
|
||
document.getElementById('statPhase').textContent = `0/${PHASES.length}`;
|
||
document.getElementById('logPanel').innerHTML = '';
|
||
document.getElementById('btnNext').disabled = false;
|
||
renderPhaseList();
|
||
log('↺ تمت إعادة تشغيل المحاكاة', 'info');
|
||
showToast('↺ تمت إعادة التشغيل', '#64748b');
|
||
nextPhase(); // auto-start noRide
|
||
}
|
||
|
||
function updateSpeed(val) {
|
||
simState.simSpeed = parseFloat(val);
|
||
document.getElementById('speedLabel').textContent = `${val}×`;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// INIT
|
||
// ═══════════════════════════════════════════════════════
|
||
window.onload = () => {
|
||
canvas = document.getElementById('mapCanvas');
|
||
ctx = canvas.getContext('2d');
|
||
|
||
function resize() {
|
||
canvas.width = canvas.offsetWidth;
|
||
canvas.height = canvas.offsetHeight;
|
||
}
|
||
resize();
|
||
window.addEventListener('resize', resize);
|
||
|
||
render();
|
||
renderPhaseList();
|
||
log('🚀 Siro Rider — محاكاة دورة حياة الرحلة محمّلة', 'ok');
|
||
log('اضغط "المرحلة التالية" أو "تشغيل تلقائي" لبدء المحاكاة', 'info');
|
||
|
||
// Kick off phase 0
|
||
setTimeout(() => {
|
||
nextPhase();
|
||
}, 500);
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|