Files
Siro/knowledge/siro_ride_simulation.html
2026-06-19 01:47:48 +03:00

1205 lines
42 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>