Files
Siro/knowledge/siro_driver_ride_simulation.html
2026-06-19 14:01:15 +03:00

2325 lines
73 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Siro Driver - Ride Lifecycle Simulator</title>
<!-- Google Fonts: Outfit -->
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
:root {
--bg-dark: #080b11;
--card-bg: rgba(13, 18, 29, 0.75);
--card-border: rgba(255, 255, 255, 0.08);
--accent-gold: #f59e0b;
--accent-gold-rgb: 245, 158, 11;
--accent-blue: #3b82f6;
--accent-emerald: #10b981;
--accent-emerald-rgb: 16, 185, 129;
--accent-rose: #f43f5e;
--text-main: #f3f4f6;
--text-muted: #9ca3af;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Outfit', sans-serif;
-webkit-font-smoothing: antialiased;
}
body {
background-color: var(--bg-dark);
color: var(--text-main);
overflow-x: hidden;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Premium Header */
header {
background: linear-gradient(to bottom, rgba(8, 11, 17, 0.95), rgba(8, 11, 17, 0));
padding: 1.5rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--card-border);
backdrop-filter: blur(10px);
z-index: 100;
}
.logo-container {
display: flex;
align-items: center;
gap: 0.75rem;
}
.logo-badge {
background: linear-gradient(135deg, var(--accent-gold), #d97706);
color: #000;
padding: 0.4rem 0.8rem;
border-radius: 8px;
font-weight: 800;
font-size: 1.1rem;
letter-spacing: 1px;
box-shadow: 0 0 15px rgba(245, 158, 11, 0.3);
}
.logo-title {
font-size: 1.3rem;
font-weight: 700;
letter-spacing: 0.5px;
}
.logo-title span {
color: var(--accent-gold);
}
.system-status {
display: flex;
align-items: center;
gap: 1.5rem;
font-size: 0.9rem;
color: var(--text-muted);
}
.status-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--accent-emerald);
box-shadow: 0 0 8px var(--accent-emerald);
}
/* Dashboard Layout */
.dashboard {
flex: 1;
display: grid;
grid-template-columns: 380px 1fr;
gap: 1.5rem;
padding: 1.5rem 2rem;
max-width: 1800px;
margin: 0 auto;
width: 100%;
}
/* Simulation Control Panel */
.control-panel {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 20px;
padding: 1.5rem;
backdrop-filter: blur(16px) saturate(180%);
display: flex;
flex-direction: column;
gap: 1.25rem;
height: fit-content;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
.panel-section {
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
padding-bottom: 1.25rem;
}
.panel-section:last-child {
border: none;
padding-bottom: 0;
}
.section-title {
font-size: 1rem;
font-weight: 600;
color: var(--accent-gold);
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-desc {
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 1rem;
line-height: 1.4;
}
/* Buttons & Controls */
.btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--text-main);
padding: 0.75rem 1rem;
border-radius: 10px;
cursor: pointer;
font-weight: 500;
font-size: 0.9rem;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
margin-bottom: 0.5rem;
}
.btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
}
.btn-gold {
background: var(--accent-gold);
color: #000;
font-weight: 600;
border: none;
}
.btn-gold:hover {
background: #e08e0b;
box-shadow: 0 0 15px rgba(245, 158, 11, 0.4);
}
.btn-rose {
background: rgba(244, 63, 94, 0.15);
border-color: rgba(244, 63, 94, 0.3);
color: var(--accent-rose);
}
.btn-rose:hover {
background: rgba(244, 63, 94, 0.25);
}
.switch-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.switch-label {
font-size: 0.9rem;
font-weight: 500;
}
.switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.1);
transition: .4s;
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 3px;
bottom: 3px;
background-color: var(--text-main);
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--accent-gold);
}
input:checked + .slider:before {
transform: translateX(20px);
background-color: #000;
}
/* Info Badge / Indicator */
.info-badge {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--card-border);
border-radius: 8px;
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.info-val {
font-weight: 600;
}
.info-val.emerald { color: var(--accent-emerald); }
.info-val.gold { color: var(--accent-gold); }
.info-val.rose { color: var(--accent-rose); }
/* Main Simulator Space */
.simulator-workspace {
display: grid;
grid-template-columns: 1fr 380px;
gap: 1.5rem;
}
/* Map Mock Component */
.map-container {
background: #0f1322;
border: 1px solid var(--card-border);
border-radius: 20px;
position: relative;
overflow: hidden;
min-height: 550px;
box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.8), 0 10px 30px rgba(0, 0, 0, 0.5);
}
/* Map Elements */
.map-grid {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: 40px 40px;
background-image:
linear-gradient(to right, rgba(255, 255, 255, 0.015) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255, 255, 255, 0.015) 1px, transparent 1px);
}
.map-road {
position: absolute;
background-color: rgba(255, 255, 255, 0.04);
border-radius: 8px;
}
.road-h { height: 16px; width: 120%; left: -10%; }
.road-v { width: 16px; height: 120%; top: -10%; }
/* Heatmap Grids overlay */
.heatmap-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(10, 1fr);
grid-template-rows: repeat(10, 1fr);
pointer-events: none;
opacity: 0;
transition: opacity 0.5s ease;
}
.heatmap-grid {
border: 1px solid transparent;
}
.heatmap-grid.high {
background-color: rgba(239, 68, 68, 0.25);
border-color: rgba(239, 68, 68, 0.6);
}
.heatmap-grid.med {
background-color: rgba(245, 158, 11, 0.25);
border-color: rgba(245, 158, 11, 0.6);
}
.heatmap-grid.low {
background-color: rgba(234, 179, 8, 0.15);
border-color: rgba(234, 179, 8, 0.4);
}
/* Route Lines drawing */
.route-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
/* Markers */
.marker {
position: absolute;
transform: translate(-50%, -50%);
transition: top 0.5s cubic-bezier(0.25, 1, 0.5, 1), left 0.5s cubic-bezier(0.25, 1, 0.5, 1), transform 0.5s ease;
display: flex;
flex-direction: column;
align-items: center;
z-index: 10;
}
.marker-dot {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.6);
}
.marker-car {
background: var(--accent-emerald);
border: 2px solid #fff;
}
.marker-pax {
background: var(--accent-gold);
border: 2px solid #000;
}
.marker-dest {
background: var(--accent-rose);
border: 2px solid #fff;
}
.marker-label {
background: rgba(0, 0, 0, 0.85);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 0.2rem 0.5rem;
border-radius: 6px;
font-size: 0.75rem;
margin-bottom: 4px;
white-space: nowrap;
pointer-events: none;
}
/* Pulsing radar for online waiting */
.radar-pulse {
position: absolute;
width: 120px;
height: 120px;
border-radius: 50%;
background: rgba(16, 185, 129, 0.05);
border: 1px solid rgba(16, 185, 129, 0.3);
transform: translate(-50%, -50%);
animation: pulse 2s infinite linear;
pointer-events: none;
display: none;
}
@keyframes pulse {
0% { transform: translate(-50%, -50%) scale(0.3); opacity: 1; }
100% { transform: translate(-50%, -50%) scale(1.5); opacity: 0; }
}
/* Mock Driver App Phone UI */
.phone-container {
background: #0b0e14;
border: 6px solid #1f2937;
border-radius: 36px;
height: 550px;
width: 100%;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
}
/* Phone Notch */
.phone-notch {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 130px;
height: 18px;
background: #1f2937;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
z-index: 50;
}
/* App StatusBar */
.app-statusbar {
height: 28px;
background: rgba(11, 14, 20, 0.9);
padding: 0 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.75rem;
font-weight: 500;
color: #9ca3af;
z-index: 40;
}
/* App Content Pane */
.app-screen {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
}
/* App Active Header Card */
.app-header-card {
background: linear-gradient(180deg, rgba(13, 18, 29, 0.95) 0%, rgba(13, 18, 29, 0.85) 100%);
border-bottom: 1px solid var(--card-border);
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 20;
}
.driver-profile {
display: flex;
align-items: center;
gap: 0.75rem;
}
.driver-avatar {
width: 38px;
height: 38px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent-gold), #d97706);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: #000;
}
.driver-info-text h4 {
font-size: 0.9rem;
font-weight: 600;
}
.driver-info-text span {
font-size: 0.75rem;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 0.25rem;
}
.app-earnings {
text-align: right;
}
.app-earnings span {
font-size: 0.7rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.app-earnings h3 {
font-size: 1.1rem;
font-weight: 700;
color: var(--accent-gold);
}
/* Bottom Control panel of App */
.app-bottom-panel {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background: rgba(13, 18, 29, 0.95);
border-top: 1px solid var(--card-border);
backdrop-filter: blur(20px);
padding: 1.25rem;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
z-index: 30;
transition: transform 0.3s ease;
}
.offline-view {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.offline-title {
font-size: 1.1rem;
font-weight: 700;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Slider Button (Go Online gesture) */
.slider-btn-container {
width: 100%;
height: 52px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--card-border);
border-radius: 26px;
position: relative;
overflow: hidden;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.slider-text {
font-size: 0.95rem;
font-weight: 600;
color: var(--text-muted);
letter-spacing: 0.5px;
pointer-events: none;
z-index: 2;
}
.slider-handle {
position: absolute;
left: 3px;
top: 3px;
width: 46px;
height: 46px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent-gold), #d97706);
display: flex;
align-items: center;
justify-content: center;
color: #000;
box-shadow: 0 0 10px rgba(245, 158, 11, 0.3);
cursor: grab;
z-index: 3;
}
.slider-bg-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 0;
background: rgba(245, 158, 11, 0.15);
z-index: 1;
}
/* Online Waiting View */
.online-view {
display: none;
flex-direction: column;
align-items: center;
gap: 0.75rem;
text-align: center;
}
.online-pulse-circle {
width: 48px;
height: 48px;
border-radius: 50%;
background: rgba(16, 185, 129, 0.1);
border: 2px solid var(--accent-emerald);
display: flex;
align-items: center;
justify-content: center;
color: var(--accent-emerald);
animation: float 2s infinite ease-in-out;
}
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-6px); }
100% { transform: translateY(0px); }
}
.online-status-title {
font-size: 1.05rem;
font-weight: 700;
color: var(--accent-emerald);
}
.online-status-subtitle {
font-size: 0.8rem;
color: var(--text-muted);
}
/* New Order Modal (Overlay Mode) */
.order-overlay-modal {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(8, 11, 17, 0.95);
z-index: 60;
display: none;
flex-direction: column;
justify-content: space-between;
padding: 2rem 1.5rem 1.5rem;
}
.order-overlay-header {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.order-badge-container {
background: rgba(245, 158, 11, 0.1);
border: 1px solid var(--accent-gold);
color: var(--accent-gold);
padding: 0.35rem 0.8rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 0.35rem;
}
.order-timer-circle {
position: relative;
width: 90px;
height: 90px;
margin: 1rem 0;
}
.timer-svg {
transform: rotate(-90deg);
width: 90px;
height: 90px;
}
.timer-track {
fill: none;
stroke: rgba(255, 255, 255, 0.05);
stroke-width: 6;
}
.timer-fill {
fill: none;
stroke: var(--accent-gold);
stroke-width: 6;
stroke-linecap: round;
stroke-dasharray: 251.2;
stroke-dashoffset: 0;
transition: stroke-dashoffset 1s linear;
}
.timer-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 1.8rem;
font-weight: 800;
color: var(--text-main);
}
/* Details Card */
.order-details-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.detail-row {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
}
.detail-label {
color: var(--text-muted);
}
.detail-val {
font-weight: 600;
}
.overlay-accept-btn {
height: 52px;
background: linear-gradient(135deg, var(--accent-emerald), #059669);
color: #000;
font-weight: 700;
font-size: 1.05rem;
border: none;
border-radius: 26px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
box-shadow: 0 0 15px rgba(16, 185, 129, 0.3);
transition: transform 0.1s ease;
}
.overlay-accept-btn:active {
transform: scale(0.98);
}
/* Ride Screen (Pickup & Trip) */
.ride-info-view {
display: none;
flex-direction: column;
gap: 0.75rem;
}
.ride-status-badge {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
color: var(--text-muted);
}
.badge-indicator {
display: flex;
align-items: center;
gap: 0.35rem;
font-weight: 600;
}
.badge-indicator.pickup { color: var(--accent-gold); }
.badge-indicator.trip { color: var(--accent-blue); }
.passenger-strip {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 0.65rem 0.75rem;
}
.passenger-strip-left {
display: flex;
align-items: center;
gap: 0.5rem;
}
.pax-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #374151;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
font-weight: 600;
}
.pax-name {
font-size: 0.85rem;
font-weight: 600;
}
.pax-rating {
font-size: 0.75rem;
color: var(--accent-gold);
display: flex;
align-items: center;
gap: 0.1rem;
}
.pax-action-icons {
display: flex;
gap: 0.5rem;
}
.circle-icon-btn {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-main);
cursor: pointer;
}
.circle-icon-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.trip-metrics-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.metric-box {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--card-border);
border-radius: 10px;
padding: 0.5rem;
display: flex;
flex-direction: column;
}
.metric-label {
font-size: 0.7rem;
color: var(--text-muted);
}
.metric-val {
font-size: 1rem;
font-weight: 700;
color: var(--accent-gold);
}
/* Turn-by-Turn Instruction Panel */
.tbt-panel {
position: absolute;
top: 10px;
left: 10px;
right: 10px;
background: rgba(13, 18, 29, 0.95);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 0.75rem;
display: none;
align-items: center;
gap: 0.75rem;
z-index: 25;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
}
.tbt-icon-box {
width: 36px;
height: 36px;
border-radius: 8px;
background: var(--accent-blue);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.tbt-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.tbt-instruction {
font-size: 0.8rem;
font-weight: 600;
}
.tbt-dist {
font-size: 0.7rem;
color: var(--accent-blue);
font-weight: 500;
}
/* Swipe gesture for Start/End Ride */
.swipe-container {
width: 100%;
height: 52px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--card-border);
border-radius: 26px;
position: relative;
overflow: hidden;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.swipe-text {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-muted);
pointer-events: none;
z-index: 2;
}
.swipe-handle {
position: absolute;
left: 3px;
top: 3px;
width: 46px;
height: 46px;
border-radius: 50%;
background: var(--accent-blue);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
z-index: 3;
cursor: grab;
}
.swipe-handle.rose {
background: var(--accent-rose);
}
/* Star rating widget */
.rating-view {
display: none;
flex-direction: column;
align-items: center;
gap: 1rem;
text-align: center;
}
.stars-row {
display: flex;
gap: 0.5rem;
margin: 0.5rem 0;
}
.star-icon {
color: #374151;
cursor: pointer;
transition: color 0.15s ease;
}
.star-icon.filled {
color: var(--accent-gold);
}
/* Stats sidebar / logs */
.stats-sidebar {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 20px;
padding: 1.5rem;
backdrop-filter: blur(16px) saturate(180%);
display: flex;
flex-direction: column;
gap: 1rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.stat-card {
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat-label {
font-size: 0.75rem;
color: var(--text-muted);
}
.stat-val {
font-size: 1.1rem;
font-weight: 700;
}
.stat-val.emerald { color: var(--accent-emerald); }
.stat-val.gold { color: var(--accent-gold); }
.stat-val.blue { color: var(--accent-blue); }
/* Performance widgets */
.perf-bar-bg {
width: 100%;
height: 6px;
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
overflow: hidden;
margin-top: 4px;
}
.perf-bar-fill {
height: 100%;
background-color: var(--accent-emerald);
width: 10%;
transition: width 0.5s ease, background-color 0.5s ease;
}
/* Console Logger */
.console-logs {
background: rgba(0, 0, 0, 0.6);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 0.75rem;
font-family: monospace;
font-size: 0.75rem;
color: #a7f3d0;
height: 160px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.log-item {
line-height: 1.3;
border-left: 2px solid var(--accent-gold);
padding-left: 0.5rem;
}
.log-item.error {
border-color: var(--accent-rose);
color: #fca5a5;
}
.log-item.info {
border-color: var(--accent-blue);
color: #93c5fd;
}
/* Arabic Notification overlay popup styling */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(5px);
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
}
.dialog-box {
background: #0d121d;
border: 1px solid var(--accent-rose);
border-radius: 16px;
padding: 1.5rem;
width: 320px;
text-align: center;
box-shadow: 0 10px 25px rgba(244, 63, 148, 0.15);
}
.dialog-icon {
font-size: 2.5rem;
color: var(--accent-rose);
margin-bottom: 0.75rem;
}
.dialog-title {
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.dialog-body {
font-size: 0.85rem;
color: var(--text-muted);
margin-bottom: 1.25rem;
line-height: 1.4;
}
.dialog-btn {
padding: 0.65rem 1.25rem;
background: var(--accent-rose);
color: #fff;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
width: 100%;
}
</style>
</head>
<body>
<!-- Header -->
<header>
<div class="logo-container">
<div class="logo-badge">SIRO</div>
<div class="logo-title">Captain <span>Ride Simulator</span></div>
</div>
<div class="system-status">
<div class="status-item">
<div class="status-dot"></div>
<span>MapLibre Engine: Active</span>
</div>
<div class="status-item">
<i data-lucide="cpu" style="width: 16px; height: 16px;"></i>
<span>Optimized Mode</span>
</div>
</div>
</header>
<!-- Dashboard -->
<div class="dashboard">
<!-- Control Panel -->
<div class="control-panel">
<!-- Driver state overrides -->
<div class="panel-section">
<div class="section-title">
<i data-lucide="sliders" style="width: 18px; height: 18px;"></i>
<span>Simulation Overrides</span>
</div>
<p class="section-desc">Alter driver variables to trigger penalties, fatigue checks, or same-device warnings.</p>
<div class="switch-container">
<span class="switch-label">Restrict Points (-201 Points)</span>
<label class="switch">
<input type="checkbox" id="chk-points">
<span class="slider"></span>
</label>
</div>
<div class="switch-container">
<span class="switch-label">Exceed Fatigue (12h limit)</span>
<label class="switch">
<input type="checkbox" id="chk-fatigue">
<span class="slider"></span>
</label>
</div>
<div class="switch-container">
<span class="switch-label">Cancellation Limit Block (3x)</span>
<label class="switch">
<input type="checkbox" id="chk-block">
<span class="slider"></span>
</label>
</div>
</div>
<!-- Trigger Events -->
<div class="panel-section">
<div class="section-title">
<i data-lucide="radio" style="width: 18px; height: 18px;"></i>
<span>Lifecycle Controls</span>
</div>
<p class="section-desc">Simulate backend and device lifecycle events during different ride stages.</p>
<button class="btn btn-gold" id="btn-trigger-order">
<i data-lucide="bell" style="width: 16px; height: 16px;"></i>
<span>Simulate Incoming Order</span>
</button>
<button class="btn" id="btn-move-closer" disabled>
<i data-lucide="navigation" style="width: 16px; height: 16px;"></i>
<span>Drive Closer (< 100m)</span>
</button>
<button class="btn" id="btn-same-device" disabled>
<i data-lucide="smartphone" style="width: 16px; height: 16px;"></i>
<span>Simulate Same Device</span>
</button>
<button class="btn btn-rose" id="btn-simulate-cancel" disabled>
<i data-lucide="user-minus" style="width: 16px; height: 16px;"></i>
<span>Simulate Rider Cancellation</span>
</button>
</div>
<!-- Device Stats -->
<div class="panel-section">
<div class="section-title">
<i data-lucide="activity" style="width: 18px; height: 18px;"></i>
<span>System Optimization Status</span>
</div>
<div style="display: flex; flex-direction: column; gap: 0.6rem;">
<div>
<div style="display: flex; justify-content: space-between; font-size: 0.75rem;">
<span style="color: var(--text-muted);">GPS Stream Pull Rate</span>
<span class="info-val emerald" id="txt-gps-rate">Centralized (1 Stream)</span>
</div>
</div>
<div>
<div style="display: flex; justify-content: space-between; font-size: 0.75rem;">
<span style="color: var(--text-muted);">Camera Follow Interval</span>
<span class="info-val emerald" id="txt-cam-interval">Throttled (8s / 15m)</span>
</div>
</div>
<div>
<div style="display: flex; justify-content: space-between; font-size: 0.75rem;">
<span style="color: var(--text-muted);">GPS Jitter Noise Filter</span>
<span class="info-val emerald">Active (> 3 meters)</span>
</div>
</div>
</div>
</div>
</div>
<!-- Main Simulator Workspace -->
<div class="simulator-workspace">
<!-- Map Visualiser -->
<div class="map-container">
<div class="map-grid"></div>
<!-- Standard mock roads -->
<div class="map-road road-h" style="top: 250px;"></div>
<div class="map-road road-v" style="left: 450px;"></div>
<div class="map-road road-h" style="top: 450px;"></div>
<div class="map-road road-v" style="left: 150px;"></div>
<!-- Heatmap Overlay Layer -->
<div class="heatmap-layer" id="heatmap-layer">
<div class="heatmap-grid high" style="grid-area: 3/4/5/6;"></div>
<div class="heatmap-grid med" style="grid-area: 2/5/3/6;"></div>
<div class="heatmap-grid med" style="grid-area: 3/6/5/7;"></div>
<div class="heatmap-grid low" style="grid-area: 5/5/7/7;"></div>
<div class="heatmap-grid high" style="grid-area: 7/2/9/4;"></div>
<div class="heatmap-grid med" style="grid-area: 6/3/7/4;"></div>
</div>
<!-- Canvas for routes -->
<canvas class="route-canvas" id="route-canvas" width="800" height="550"></canvas>
<!-- Radar pulse for searching -->
<div class="radar-pulse" id="radar-pulse"></div>
<!-- Map Markers -->
<!-- Driver Car Marker -->
<div class="marker" id="marker-driver" style="top: 250px; left: 150px; transform: translate(-50%, -50%) rotate(0deg);">
<div class="marker-label">Damascus Driver (You)</div>
<div class="marker-dot marker-car">
<i data-lucide="car" style="width: 14px; height: 14px; color: #000;"></i>
</div>
</div>
<!-- Passenger Marker -->
<div class="marker" id="marker-pax" style="top: 250px; left: 450px; display: none;">
<div class="marker-label" id="pax-label">Mahmoud (Pickup)</div>
<div class="marker-dot marker-pax">
<i data-lucide="user" style="width: 14px; height: 14px; color: #000;"></i>
</div>
</div>
<!-- Destination Marker -->
<div class="marker" id="marker-dest" style="top: 450px; left: 450px; display: none;">
<div class="marker-label" id="dest-label">Mezzeh (Destination)</div>
<div class="marker-dot marker-dest">
<i data-lucide="flag" style="width: 14px; height: 14px; color: #fff;"></i>
</div>
</div>
<!-- Turn-by-Turn Instruction Panel -->
<div class="tbt-panel" id="tbt-panel">
<div class="tbt-icon-box" id="tbt-icon">
<i data-lucide="arrow-up-right" style="width: 20px; height: 20px;"></i>
</div>
<div class="tbt-info">
<span class="tbt-instruction" id="tbt-text">Drive straight on Al-Jalaa St</span>
<span class="tbt-dist" id="tbt-dist">500 m</span>
</div>
</div>
</div>
<!-- App Interface Wrapper -->
<div>
<div class="phone-container">
<div class="phone-notch"></div>
<!-- App Status bar -->
<div class="app-statusbar">
<span id="txt-app-time">01:31</span>
<div style="display: flex; align-items: center; gap: 0.35rem;">
<i data-lucide="wifi" style="width: 12px; height: 12px;"></i>
<i data-lucide="battery" style="width: 14px; height: 14px;"></i>
</div>
</div>
<!-- App Screen -->
<div class="app-screen">
<!-- Active Header -->
<div class="app-header-card">
<div class="driver-profile">
<div class="driver-avatar">C</div>
<div class="driver-info-text">
<h4>Capt. Hamza</h4>
<span id="txt-points-badge"><i data-lucide="award" style="width: 12px; height: 12px; color: var(--accent-gold);"></i> 180 Points</span>
</div>
</div>
<div class="app-earnings">
<span>Today's Cash</span>
<h3 id="txt-app-earnings">0 S.P</h3>
</div>
</div>
<!-- New Order Modal overlay inside the screen -->
<div class="order-overlay-modal" id="order-overlay">
<div class="order-overlay-header">
<div class="order-badge-container">
<i data-lucide="zap" style="width: 12px; height: 12px;"></i>
<span id="txt-ride-type">COMFORT REQUEST</span>
</div>
<div class="order-timer-circle">
<svg class="timer-svg">
<circle class="timer-track" cx="45" cy="45" r="40"></circle>
<circle class="timer-fill" id="timer-fill" cx="45" cy="45" r="40"></circle>
</svg>
<div class="timer-text" id="txt-overlay-timer">15</div>
</div>
</div>
<div class="order-details-card">
<div class="detail-row">
<span class="detail-label">Passenger:</span>
<span class="detail-val">Mahmoud K. (★ 4.9)</span>
</div>
<div class="detail-row">
<span class="detail-label">Pickup Distance:</span>
<span class="detail-val" id="txt-overlay-pickup">2.1 km away</span>
</div>
<div class="detail-row">
<span class="detail-label">Trip Length:</span>
<span class="detail-val" id="txt-overlay-length">12.5 km</span>
</div>
<div class="detail-row">
<span class="detail-label">Est. Price:</span>
<span class="detail-val" style="color: var(--accent-gold);" id="txt-overlay-price">28,000 S.P.</span>
</div>
</div>
<button class="overlay-accept-btn" id="btn-accept-order">
<i data-lucide="check-circle" style="width: 20px; height: 20px;"></i>
<span>Accept Order (قبول الطلب)</span>
</button>
</div>
<!-- Bottom Dashboard View (Switches based on state) -->
<div class="app-bottom-panel" id="app-bottom-panel">
<!-- Offline Panel -->
<div class="offline-view" id="panel-offline">
<div class="offline-title">
<i data-lucide="cloud-off" style="width: 20px; height: 20px;"></i>
<span>You are Offline</span>
</div>
<div class="slider-btn-container" id="online-slider-container">
<div class="slider-bg-fill" id="online-slider-fill"></div>
<div class="slider-handle" id="online-slider-handle">
<i data-lucide="chevrons-right" style="width: 18px; height: 18px;"></i>
</div>
<span class="slider-text" id="online-slider-text">SWIPE GO ONLINE</span>
</div>
</div>
<!-- Online Waiting Panel -->
<div class="online-view" id="panel-online">
<div class="online-pulse-circle">
<i data-lucide="radio" style="width: 20px; height: 20px;"></i>
</div>
<div class="online-status-title">Looking for Rides...</div>
<div class="online-status-subtitle">Damascus demand heatmap sync active</div>
<div style="display: flex; gap: 0.5rem; width: 100%; margin-top: 0.5rem;">
<button class="btn" id="btn-toggle-heatmap" style="flex: 1; font-size: 0.8rem; margin: 0; padding: 0.5rem;">
<i data-lucide="map" style="width: 14px; height: 14px;"></i>
<span>Toggle Heatmap</span>
</button>
<button class="btn btn-rose" id="btn-go-offline" style="flex: 1; font-size: 0.8rem; margin: 0; padding: 0.5rem;">
<span>Go Offline</span>
</button>
</div>
</div>
<!-- Active Ride Panel -->
<div class="ride-info-view" id="panel-ride-active">
<div class="ride-status-badge">
<span>RIDE LIFECYCLE</span>
<div class="badge-indicator pickup" id="txt-lifecycle-phase">
<div class="status-dot" style="background-color: var(--accent-gold); box-shadow: 0 0 8px var(--accent-gold);"></div>
<span>Heading to Pickup</span>
</div>
</div>
<div class="passenger-strip">
<div class="passenger-strip-left">
<div class="pax-avatar">M</div>
<div>
<div class="pax-name">Mahmoud K.</div>
<div class="pax-rating">
<i data-lucide="star" style="width: 12px; height: 12px; fill: var(--accent-gold); color: transparent;"></i>
<span>4.9</span>
</div>
</div>
</div>
<div class="pax-action-icons">
<div class="circle-icon-btn" id="btn-pax-call">
<i data-lucide="phone" style="width: 14px; height: 14px;"></i>
</div>
<div class="circle-icon-btn">
<i data-lucide="message-square" style="width: 14px; height: 14px;"></i>
</div>
</div>
</div>
<div class="trip-metrics-grid">
<div class="metric-box">
<span class="metric-label" id="txt-metric-top-label">Pickup Dist</span>
<span class="metric-val" id="txt-metric-top-val">2.1 km</span>
</div>
<div class="metric-box">
<span class="metric-label" id="txt-metric-bottom-label">Est. Time</span>
<span class="metric-val" id="txt-metric-bottom-val">5 min</span>
</div>
</div>
<!-- Swipe to start/finish -->
<div class="swipe-container" id="ride-swipe-container" style="display: none;">
<div class="slider-bg-fill" id="ride-swipe-fill" style="background: rgba(59, 130, 246, 0.15);"></div>
<div class="swipe-handle" id="ride-swipe-handle">
<i data-lucide="chevrons-right" style="width: 18px; height: 18px;"></i>
</div>
<span class="swipe-text" id="ride-swipe-text">SWIPE START RIDE</span>
</div>
<!-- Arrived Action button -->
<button class="btn btn-gold" id="btn-arrive-action" style="margin: 0; font-weight: 700; height: 50px; border-radius: 25px;">
<span>MARK ARRIVED (وصلت)</span>
</button>
</div>
<!-- Rating View -->
<div class="rating-view" id="panel-rating">
<i data-lucide="check-check" style="width: 36px; height: 36px; color: var(--accent-emerald);"></i>
<h4 style="font-size: 1rem; font-weight: 700;">Ride Completed Successfully!</h4>
<p style="font-size: 0.8rem; color: var(--text-muted);">Please rate passenger Mahmoud K.</p>
<div class="stars-row">
<i data-lucide="star" class="star-icon" data-idx="1" style="width: 24px; height: 24px;"></i>
<i data-lucide="star" class="star-icon" data-idx="2" style="width: 24px; height: 24px;"></i>
<i data-lucide="star" class="star-icon" data-idx="3" style="width: 24px; height: 24px;"></i>
<i data-lucide="star" class="star-icon" data-idx="4" style="width: 24px; height: 24px;"></i>
<i data-lucide="star" class="star-icon" data-idx="5" style="width: 24px; height: 24px;"></i>
</div>
<button class="btn btn-gold" id="btn-submit-rating" style="margin: 0;">Submit Rating</button>
</div>
</div>
</div>
</div>
<!-- Simulation Logs & Info -->
<div class="stats-sidebar" style="margin-top: 1.5rem;">
<div class="section-title">
<i data-lucide="terminal" style="width: 18px; height: 18px;"></i>
<span>Real-time Execution Log</span>
</div>
<div class="console-logs" id="console-logs">
<div class="log-item info">System initialized. Driver is Offline.</div>
</div>
</div>
</div>
</div>
</div>
<!-- Custom Alert overlay dialog -->
<div class="dialog-overlay" id="dialog-overlay">
<div class="dialog-box">
<div class="dialog-icon"></div>
<div class="dialog-title" id="dialog-title">Blocked</div>
<div class="dialog-body" id="dialog-body">You have exceeded the cancellation limit.</div>
<button class="dialog-btn" id="dialog-btn">OK</button>
</div>
</div>
<script>
// System variables
let driverState = 'OFFLINE'; // OFFLINE, ONLINE, NEW_ORDER, ACCEPTED, ARRIVED, EN_ROUTE, COMPLETED
let driverPoints = 180;
let driverCoins = 0;
let isBlocked = false;
let isFatigued = false;
let isPointsRestricted = false;
let blockExpiryTime = null;
let totalTimeOnlineToday = 0; // seconds
let onlineTimer = null;
// Map geometry
let driverPos = { x: 150, y: 250 }; // Pixels
let paxPos = { x: 450, y: 250 };
let destPos = { x: 450, y: 450 };
let carRotation = 0;
// Simulation variables
let rideId = 1059;
let priceCalculated = 28000;
let distanceCoveredKm = 0.0;
let rideSecondsElapsed = 0;
let rideCostTimer = null;
let waitTimer = null;
let waitSecondsRemaining = 300;
let routeCanvas = document.getElementById('route-canvas');
let ctx = routeCanvas.getContext('2d');
// UI Selectors
const consoleLogs = document.getElementById('console-logs');
function addLog(text, type = 'default') {
const log = document.createElement('div');
log.className = `log-item ${type}`;
log.textContent = `[${new Date().toLocaleTimeString()}] ${text}`;
consoleLogs.appendChild(log);
consoleLogs.scrollTop = consoleLogs.scrollHeight;
}
// Initialize icons
lucide.createIcons();
// System Alert dialog helper
function showSystemDialog(title, body, isBlockDialog = true) {
document.getElementById('dialog-title').innerText = title;
document.getElementById('dialog-body').innerText = body;
document.getElementById('dialog-overlay').style.display = 'flex';
const btn = document.getElementById('dialog-btn');
btn.onclick = () => {
document.getElementById('dialog-overlay').style.display = 'none';
};
}
// Swipe Go Online controller gesture
const handle = document.getElementById('online-slider-handle');
const container = document.getElementById('online-slider-container');
const fill = document.getElementById('online-slider-fill');
let isDragging = false;
let startX = 0;
let containerWidth = container.clientWidth;
handle.addEventListener('mousedown', startDrag);
handle.addEventListener('touchstart', startDrag);
function startDrag(e) {
if (driverState !== 'OFFLINE') return;
isDragging = true;
startX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
document.addEventListener('mousemove', onDrag);
document.addEventListener('touchmove', onDrag);
document.addEventListener('mouseup', stopDrag);
document.addEventListener('touchend', stopDrag);
handle.style.cursor = 'grabbing';
}
function onDrag(e) {
if (!isDragging) return;
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
let deltaX = clientX - startX;
let limit = container.clientWidth - handle.clientWidth - 6;
if (deltaX < 0) deltaX = 0;
if (deltaX > limit) deltaX = limit;
handle.style.left = (deltaX + 3) + 'px';
fill.style.width = (deltaX + 23) + 'px';
// Check threshold (swipe confirmation)
if (deltaX >= limit * 0.95) {
// Trigger Go Online
confirmGoOnline();
stopDrag();
}
}
function stopDrag() {
if (!isDragging) return;
isDragging = false;
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('touchmove', onDrag);
handle.style.cursor = 'grab';
if (driverState === 'OFFLINE') {
// Revert handle position
handle.style.transition = 'left 0.2s ease';
fill.style.transition = 'width 0.2s ease';
handle.style.left = '3px';
fill.style.width = '0px';
setTimeout(() => {
handle.style.transition = '';
fill.style.transition = '';
}, 200);
}
}
// Go Online Check Logic
function confirmGoOnline() {
// 1. Check points restriction
if (isPointsRestricted) {
addLog("Go Online Rejected: Account points below Syria threshold (-200 points).", "error");
showSystemDialog("Stopped due to low points! ⛔", "Your account points (-201) are below the Damascus region minimum threshold (-200). Please recharge your wallet to proceed.");
resetOnlineSlider();
return;
}
// 2. Check cancellation block limit
if (isBlocked) {
addLog("Go Online Rejected: Temporarily blocked due to excessive cancellations.", "error");
showSystemDialog("Account restricted! ⛔", "You have exceeded the cancellation limit (3 times). You cannot go online for the next 4 hours.");
resetOnlineSlider();
return;
}
// 3. Check fatigue (12 hours)
if (isFatigued) {
addLog("Go Online Rejected: Driving hours exceeded 12-hour safety limit.", "error");
showSystemDialog("Safety break required! 🛑", "You have been driving for 12 hours today. For your safety, you must take a continuous 6-hour break.");
resetOnlineSlider();
return;
}
// Successful online state
setOnlineState();
}
function resetOnlineSlider() {
handle.style.left = '3px';
fill.style.width = '0px';
}
function setOnlineState() {
driverState = 'ONLINE';
addLog("Driver went Online. Throttled GPS listener active.", "info");
document.getElementById('panel-offline').style.display = 'none';
document.getElementById('panel-online').style.display = 'flex';
document.getElementById('radar-pulse').style.display = 'block';
document.getElementById('radar-pulse').style.left = driverPos.x + 'px';
document.getElementById('radar-pulse').style.top = driverPos.y + 'px';
// Start online duration accumulator to monitor fatigue
onlineTimer = setInterval(() => {
totalTimeOnlineToday++;
if (totalTimeOnlineToday >= 60) {
// Accelerate simulation: 1 min online acts as 12 hours fatigue trigger
isFatigued = true;
clearInterval(onlineTimer);
addLog("Simulation: 12-hour online driving fatigue threshold reached. Forcing offline.", "error");
goOffline();
showSystemDialog("Safety limit reached! 🛑", "You have driven for 12 hours today. To prevent fatigue-related hazards, you are forced offline for a 6-hour break.");
}
}, 1000);
}
function goOffline() {
driverState = 'OFFLINE';
clearInterval(onlineTimer);
addLog("Driver went Offline.", "info");
document.getElementById('panel-online').style.display = 'none';
document.getElementById('panel-offline').style.display = 'flex';
document.getElementById('radar-pulse').style.display = 'none';
resetOnlineSlider();
}
document.getElementById('btn-go-offline').onclick = goOffline;
// Toggle Heatmap Layer
let heatmapActive = false;
function toggleHeatmap() {
heatmapActive = !heatmapActive;
const layer = document.getElementById('heatmap-layer');
if (heatmapActive) {
layer.style.opacity = '1';
addLog("Demand Heatmap Synced periodic interval set to 15 min (Optimized).", "info");
} else {
layer.style.opacity = '0';
addLog("Demand Heatmap Layer hidden.", "info");
}
}
document.getElementById('btn-toggle-heatmap').onclick = toggleHeatmap;
// Trigger Incoming Order (FCM/Socket mockup)
let orderOverlay = document.getElementById('order-overlay');
let overlayTimer = 15;
let overlayInterval = null;
document.getElementById('btn-trigger-order').onclick = () => {
if (driverState !== 'ONLINE') {
addLog("Cannot trigger order. Driver must be Online first.", "error");
return;
}
triggerOrder();
};
function triggerOrder() {
driverState = 'NEW_ORDER';
addLog("New Order received via Firebase Socket (ride_id: " + rideId + ")", "info");
orderOverlay.style.display = 'flex';
document.getElementById('btn-simulate-cancel').removeAttribute('disabled');
// Sound alert (Beep mockup)
playBeepAlert();
overlayTimer = 15;
const fillEl = document.getElementById('timer-fill');
fillEl.style.strokeDashoffset = '0';
document.getElementById('txt-overlay-timer').innerText = overlayTimer;
// Draw BOTH routes on the map during the Order Request view
drawRoute(driverPos, paxPos, 'both');
// Show both markers on map
document.getElementById('marker-pax').style.display = 'flex';
document.getElementById('marker-dest').style.display = 'flex';
// Update markers to act as dynamic Info Windows (Time & Distance)
document.getElementById('pax-label').innerHTML = `Mahmoud (Pickup)<br><span style="color:var(--accent-gold); font-weight:bold; font-size:0.7rem;">2.1 km | 5 min</span>`;
document.getElementById('dest-label').innerHTML = `Mezzeh (Destination)<br><span style="color:var(--accent-blue); font-weight:bold; font-size:0.7rem;">12.5 km | 20 min</span>`;
overlayInterval = setInterval(() => {
overlayTimer--;
document.getElementById('txt-overlay-timer').innerText = overlayTimer;
// Circular dash animation
let percent = overlayTimer / 15;
let offset = 251.2 * (1 - percent);
fillEl.style.strokeDashoffset = offset;
if (overlayTimer <= 0) {
clearInterval(overlayInterval);
addLog("Order Request timed out. Missed count incremented.", "error");
orderOverlay.style.display = 'none';
driverState = 'ONLINE';
// Clear map state
document.getElementById('marker-pax').style.display = 'none';
document.getElementById('marker-dest').style.display = 'none';
ctx.clearRect(0, 0, routeCanvas.width, routeCanvas.height);
}
}, 1000);
}
function playBeepAlert() {
// Basic HTML5 beep generator
try {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime); // A5 note
gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);
oscillator.start();
setTimeout(() => oscillator.stop(), 300);
} catch (e) {}
}
// Handle same-device check warning simulation
document.getElementById('btn-same-device').onclick = () => {
addLog("GPS warning: Driver & Passenger distance < 10m. Fit bounds domain_error avoided.", "error");
showSystemDialog("Same device detected! ⚠️", "Rider and Captain locations are extremely close (under 10 meters). To prevent MapLibre fitBounds domains error crash, map will transition to approximate padded safe zoom.");
};
// Accept Order Logic
document.getElementById('btn-accept-order').onclick = () => {
acceptOrder();
};
function acceptOrder() {
clearInterval(overlayInterval);
driverState = 'ACCEPTED';
addLog("Order accepted. Transitioning to Passenger Location Map.", "info");
orderOverlay.style.display = 'none';
document.getElementById('btn-trigger-order').setAttribute('disabled', 'true');
document.getElementById('btn-move-closer').removeAttribute('disabled');
document.getElementById('btn-same-device').removeAttribute('disabled');
// Setup UI for accepted state (Heading to pickup)
document.getElementById('panel-online').style.display = 'none';
document.getElementById('panel-ride-active').style.display = 'flex';
// Show passenger marker and hide destination marker initially
document.getElementById('marker-pax').style.display = 'flex';
document.getElementById('marker-dest').style.display = 'none'; // Clear destination marker
document.getElementById('pax-label').innerHTML = `Mahmoud (Pickup)`; // Restore default label
document.getElementById('radar-pulse').style.display = 'none';
// Draw ONLY the yellow route to passenger
drawRoute(driverPos, paxPos, 'yellow');
// Update TBT instructions Panel
document.getElementById('tbt-panel').style.display = 'flex';
document.getElementById('tbt-text').innerText = "Drive east toward Mahmoud's pickup point";
document.getElementById('tbt-dist').innerText = "2.1 km";
}
// Draw lines on canvas
function drawRoute(start, end, type = 'yellow') {
ctx.clearRect(0, 0, routeCanvas.width, routeCanvas.height);
if (type === 'both') {
// 1. Draw yellow route (driver to passenger)
ctx.beginPath();
ctx.lineWidth = 6;
ctx.strokeStyle = '#eab308'; // Amber
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.stroke();
// Draw dotted walk line from road (corner) to actual offroad pax position
ctx.beginPath();
ctx.lineWidth = 3;
ctx.strokeStyle = '#94a3b8';
ctx.setLineDash([6, 6]);
ctx.moveTo(end.x, start.y);
ctx.lineTo(end.x + 20, end.y - 30);
ctx.stroke();
ctx.setLineDash([]);
// 2. Draw blue route (passenger to destination)
ctx.beginPath();
ctx.lineWidth = 6;
ctx.strokeStyle = '#3b82f6'; // Blue
ctx.moveTo(end.x, end.y);
ctx.lineTo(destPos.x, destPos.y);
ctx.stroke();
return;
}
ctx.beginPath();
ctx.lineWidth = 6;
ctx.strokeStyle = type === 'yellow' ? '#eab308' : '#3b82f6';
// Connect start to end through mockup nodes to look like roads
ctx.moveTo(start.x, start.y);
if (start.x !== end.x && start.y !== end.y) {
ctx.lineTo(end.x, start.y); // Corner bend
}
ctx.lineTo(end.x, end.y);
ctx.stroke();
// If heading to pickup, draw dotted walk line from road (corner) to actual off-road position
if (type === 'yellow') {
ctx.beginPath();
ctx.lineWidth = 3;
ctx.strokeStyle = '#94a3b8'; // Grey/Blue dashes
ctx.setLineDash([6, 6]);
ctx.moveTo(end.x, start.y);
ctx.lineTo(end.x + 20, end.y - 30); // offroad displacement mockup
ctx.stroke();
ctx.setLineDash([]); // Reset
}
}
// Simulate moving closer to pickup
document.getElementById('btn-move-closer').onclick = () => {
simulateMoveCloser();
};
function simulateMoveCloser() {
addLog("GPS update: Captain driving. Centralized polling interval tick.", "info");
// Animate driver marker moving to passenger location on map
let marker = document.getElementById('marker-driver');
driverPos = { x: 440, y: 250 }; // Closer to passenger (450, 250)
marker.style.left = driverPos.x + 'px';
marker.style.top = driverPos.y + 'px';
marker.style.transform = 'translate(-50%, -50%) rotate(90deg)';
// Draw updated route
drawRoute(driverPos, paxPos, 'yellow');
// Update UI
document.getElementById('txt-metric-top-val').innerText = "80 m";
document.getElementById('txt-metric-bottom-val').innerText = "0 min";
document.getElementById('btn-move-closer').setAttribute('disabled', 'true');
document.getElementById('btn-arrive-action').removeAttribute('disabled');
addLog("Captain is close to passenger location (< 100m). 'Mark Arrived' unlocked.", "info");
}
// Mark Arrived Action
document.getElementById('btn-arrive-action').onclick = () => {
markArrived();
};
function markArrived() {
driverState = 'ARRIVED';
addLog("Driver marked Arrived. Waiting compensation timer started.", "info");
document.getElementById('btn-arrive-action').style.display = 'none';
// Clear route line (deletes the first yellow polyline)
ctx.clearRect(0, 0, routeCanvas.width, routeCanvas.height);
// Update badge
document.getElementById('txt-lifecycle-phase').innerHTML = `
<div class="status-dot" style="background-color: var(--accent-emerald); box-shadow: 0 0 8px var(--accent-emerald);"></div>
<span>Waiting for Passenger</span>
`;
document.getElementById('txt-lifecycle-phase').className = "badge-indicator emerald";
// Setup Waiting Duration Timer
waitSecondsRemaining = 300;
document.getElementById('txt-metric-top-label').innerText = "Wait Time";
document.getElementById('txt-metric-top-val').innerText = "05:00";
document.getElementById('txt-metric-bottom-label').innerText = "Status";
document.getElementById('txt-metric-bottom-val').innerText = "Arrived";
waitTimer = setInterval(() => {
waitSecondsRemaining--;
let min = Math.floor(waitSecondsRemaining / 60);
let sec = waitSecondsRemaining % 60;
document.getElementById('txt-metric-top-val').innerText = `${min.toString().padLeft(2, '0')}:${sec.toString().padLeft(2, '0')}`;
if (waitSecondsRemaining <= 0) {
clearInterval(waitTimer);
addLog("Waiting limit of 5 min exceeded. Paid cancellation unlocked.", "rose");
document.getElementById('txt-metric-bottom-val').innerText = "No-Show";
document.getElementById('btn-simulate-cancel').innerText = "Cancel Paid (عدم صعود)";
}
}, 1000);
// Reveal Swipe to Start Ride
document.getElementById('ride-swipe-container').style.display = 'flex';
setupSwipeControl();
}
// Swipe Start/End Controller Logic
let rideSwipeHandle = document.getElementById('ride-swipe-handle');
let rideSwipeContainer = document.getElementById('ride-swipe-container');
let rideSwipeFill = document.getElementById('ride-swipe-fill');
let isSwipeActive = false;
let swipeStartX = 0;
function setupSwipeControl() {
rideSwipeHandle.addEventListener('mousedown', startSwipe);
rideSwipeHandle.addEventListener('touchstart', startSwipe);
}
function startSwipe(e) {
isSwipeActive = true;
swipeStartX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
document.addEventListener('mousemove', onSwipe);
document.addEventListener('touchmove', onSwipe);
document.addEventListener('mouseup', stopSwipe);
document.addEventListener('touchend', stopSwipe);
rideSwipeHandle.style.cursor = 'grabbing';
}
function onSwipe(e) {
if (!isSwipeActive) return;
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
let deltaX = clientX - swipeStartX;
let limit = rideSwipeContainer.clientWidth - rideSwipeHandle.clientWidth - 6;
if (deltaX < 0) deltaX = 0;
if (deltaX > limit) deltaX = limit;
rideSwipeHandle.style.left = (deltaX + 3) + 'px';
rideSwipeFill.style.width = (deltaX + 23) + 'px';
if (deltaX >= limit * 0.95) {
if (driverState === 'ARRIVED') {
startRide();
} else if (driverState === 'EN_ROUTE') {
endRideAttempt();
}
stopSwipe();
}
}
function stopSwipe() {
if (!isSwipeActive) return;
isSwipeActive = false;
document.removeEventListener('mousemove', onSwipe);
document.removeEventListener('touchmove', onSwipe);
rideSwipeHandle.style.cursor = 'grab';
// Reset swipe positions
rideSwipeHandle.style.transition = 'left 0.2s ease';
rideSwipeFill.style.transition = 'width 0.2s ease';
rideSwipeHandle.style.left = '3px';
rideSwipeFill.style.width = '0px';
setTimeout(() => {
rideSwipeHandle.style.transition = '';
rideSwipeFill.style.transition = '';
}, 200);
}
// Start Ride Logic
function startRide() {
clearInterval(waitTimer);
driverState = 'EN_ROUTE';
addLog("Ride Started. Solid navigation route updated. Voice recording initiated.", "info");
// Animate driver to original road
driverPos = { x: 450, y: 250 };
document.getElementById('marker-driver').style.left = driverPos.x + 'px';
document.getElementById('marker-driver').style.top = driverPos.y + 'px';
// Show destination marker on map
document.getElementById('marker-dest').style.display = 'flex';
document.getElementById('dest-label').innerHTML = `Mezzeh (Destination)`;
// Draw Trip Route (solid blue/black)
drawRoute(driverPos, destPos, 'blue');
// Update TBT instructions Panel
document.getElementById('tbt-text').innerText = "Drive south on Mezzeh Highway";
document.getElementById('tbt-dist').innerText = "12.5 km";
// Setup UI
document.getElementById('txt-lifecycle-phase').innerHTML = `
<div class="status-dot" style="background-color: var(--accent-blue); box-shadow: 0 0 8px var(--accent-blue);"></div>
<span>En Route to Mezzeh</span>
`;
document.getElementById('txt-lifecycle-phase').className = "badge-indicator trip";
document.getElementById('txt-metric-top-label').innerText = "Distance Covered";
document.getElementById('txt-metric-top-val').innerText = "0.0 km";
document.getElementById('txt-metric-bottom-label').innerText = "Current Cost";
document.getElementById('txt-metric-bottom-val').innerText = "28,000 S.P.";
document.getElementById('ride-swipe-text').innerText = "SWIPE END RIDE";
rideSwipeHandle.classList.add('rose');
// Start dynamic pricing and mock navigation updates
rideSecondsElapsed = 0;
distanceCoveredKm = 0.0;
rideCostTimer = setInterval(() => {
rideSecondsElapsed += 15; // Accelerate time
distanceCoveredKm += 0.25; // Accelerate distance
if (distanceCoveredKm > 12.5) distanceCoveredKm = 12.5;
// Dynamic pricing: Base (28000) + time increments + distance increments
// Using Damascus Comfort pricing: ~800 S.P/km comfort base
let updatedPrice = 28000 + (distanceCoveredKm * 800) + (rideSecondsElapsed / 60 * 150);
priceCalculated = Math.round(updatedPrice);
document.getElementById('txt-metric-top-val').innerText = `${distanceCoveredKm.toFixed(2)} km`;
document.getElementById('txt-metric-bottom-val').innerText = `${priceCalculated.toLocaleString()} S.P.`;
// Update driver position slowly on map
let fraction = distanceCoveredKm / 12.5;
let x = driverPos.x;
let y = driverPos.y + (destPos.y - driverPos.y) * fraction;
document.getElementById('marker-driver').style.top = y + 'px';
document.getElementById('marker-driver').style.transform = 'translate(-50%, -50%) rotate(180deg)';
// TBT Update
let remainingDist = (12.5 - distanceCoveredKm).toFixed(1);
document.getElementById('tbt-dist').innerText = `${remainingDist} km`;
if (distanceCoveredKm >= 12.5) {
document.getElementById('tbt-text').innerText = "Arriving at Mezzeh. End the ride.";
}
}, 1000);
}
// End Ride Attempt with Displacement Check (Anti-Fraud)
function endRideAttempt() {
// Planned trip distance: 12.5 km.
// Minimum displacement threshold: (1/5) = 2.5 km.
const minThreshold = 2.5;
if (distanceCoveredKm < minThreshold) {
// Anti-fraud validation failed
addLog("Anti-Fraud Violation: Attempted early exit at " + distanceCoveredKm.toFixed(2) + " km. Displ. under 1/5.", "error");
// Trigger Speech synthesis warning (matching Dart TTS code)
speakTTS("You haven't moved sufficiently! Please complete more distance before ending.");
showSystemDialog("Warning: Insufficient Distance! 🛑", "You haven't moved sufficiently! You must complete at least 1/5 of the total trip distance (2.5 km) before ending the ride.");
return;
}
// Successful ride completion
completeRide();
}
function speakTTS(text) {
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'en-US';
utterance.rate = 1.0;
window.speechSynthesis.speak(utterance);
}
}
function completeRide() {
clearInterval(rideCostTimer);
driverState = 'COMPLETED';
addLog("Ride completed successfully. Wallet transaction completed on server.", "info");
// Update profile wallet amount
driverCoins += priceCalculated;
document.getElementById('txt-app-earnings').innerText = `${driverCoins.toLocaleString()} S.P`;
document.getElementById('tbt-panel').style.display = 'none';
document.getElementById('panel-ride-active').style.display = 'none';
document.getElementById('panel-rating').style.display = 'flex';
}
// Star rating animation
const stars = document.querySelectorAll('.star-icon');
let ratedValue = 5;
stars.forEach(star => {
star.onclick = () => {
let idx = parseInt(star.getAttribute('data-idx'));
ratedValue = idx;
stars.forEach((s, i) => {
if (i < idx) {
s.classList.add('filled');
} else {
s.classList.remove('filled');
}
});
};
});
document.getElementById('btn-submit-rating').onclick = () => {
addLog("Rating submitted: Mahmoud K. rated " + ratedValue + " stars.", "info");
// Reset back to Online
document.getElementById('panel-rating').style.display = 'none';
document.getElementById('panel-online').style.display = 'flex';
document.getElementById('radar-pulse').style.display = 'block';
driverState = 'ONLINE';
// Reset map markers
document.getElementById('marker-pax').style.display = 'none';
document.getElementById('marker-dest').style.display = 'none';
// Reset driver position
driverPos = { x: 150, y: 250 };
document.getElementById('marker-driver').style.left = driverPos.x + 'px';
document.getElementById('marker-driver').style.top = driverPos.y + 'px';
document.getElementById('marker-driver').style.transform = 'translate(-50%, -50%) rotate(0deg)';
// Reset route
ctx.clearRect(0, 0, routeCanvas.width, routeCanvas.height);
document.getElementById('btn-trigger-order').removeAttribute('disabled');
document.getElementById('btn-arrive-action').style.display = 'block';
document.getElementById('btn-arrive-action').setAttribute('disabled', 'true');
document.getElementById('ride-swipe-container').style.display = 'none';
rideSwipeHandle.classList.remove('rose');
document.getElementById('ride-swipe-text').innerText = "SWIPE START RIDE";
};
// Override switches
document.getElementById('chk-points').onchange = (e) => {
isPointsRestricted = e.target.checked;
if (isPointsRestricted) {
driverPoints = -201;
document.getElementById('txt-points-badge').innerHTML = `<i data-lucide="award" style="width: 12px; height: 12px; color: var(--accent-rose);"></i> -201 Points`;
addLog("Override: Points restricted set to -201.", "rose");
} else {
driverPoints = 180;
document.getElementById('txt-points-badge').innerHTML = `<i data-lucide="award" style="width: 12px; height: 12px; color: var(--accent-gold);"></i> 180 Points`;
addLog("Override: Points restored to 180.", "info");
}
lucide.createIcons();
};
document.getElementById('chk-fatigue').onchange = (e) => {
isFatigued = e.target.checked;
if (isFatigued) {
addLog("Override: Simulated driving fatigue limit reached.", "rose");
} else {
totalTimeOnlineToday = 0;
addLog("Override: Fatigue limits reset.", "info");
}
};
document.getElementById('chk-block').onchange = (e) => {
isBlocked = e.target.checked;
if (isBlocked) {
addLog("Override: Driver is blocked due to exceeding cancellations today.", "rose");
} else {
addLog("Override: Cancellation block removed.", "info");
}
};
// Handle rider cancellation simulation
document.getElementById('btn-simulate-cancel').onclick = () => {
simulateRiderCancel();
};
function simulateRiderCancel() {
addLog("Rider Mahmoud cancelled the ride. Socket channel closed.", "rose");
// Clean timers
clearInterval(waitTimer);
clearInterval(rideCostTimer);
clearInterval(overlayInterval);
// Show cancel dialog
showSystemDialog("Order Cancelled 🚫", "Rider Mahmoud K. has cancelled the request. Reason: Changed plans. All navigation timers and routing have been cleaned.");
// Revert UI to Online
document.getElementById('panel-ride-active').style.display = 'none';
document.getElementById('panel-online').style.display = 'flex';
document.getElementById('radar-pulse').style.display = 'block';
document.getElementById('order-overlay').style.display = 'none';
driverState = 'ONLINE';
// Reset map markers
document.getElementById('marker-pax').style.display = 'none';
document.getElementById('marker-dest').style.display = 'none';
// Reset driver position
driverPos = { x: 150, y: 250 };
document.getElementById('marker-driver').style.left = driverPos.x + 'px';
document.getElementById('marker-driver').style.top = driverPos.y + 'px';
document.getElementById('marker-driver').style.transform = 'translate(-50%, -50%) rotate(0deg)';
// Reset route
ctx.clearRect(0, 0, routeCanvas.width, routeCanvas.height);
document.getElementById('btn-trigger-order').removeAttribute('disabled');
document.getElementById('btn-arrive-action').style.display = 'block';
document.getElementById('btn-arrive-action').setAttribute('disabled', 'true');
document.getElementById('ride-swipe-container').style.display = 'none';
rideSwipeHandle.classList.remove('rose');
document.getElementById('ride-swipe-text').innerText = "SWIPE START RIDE";
document.getElementById('btn-simulate-cancel').setAttribute('disabled', 'true');
}
// Centralized custom string padding
String.prototype.padLeft = function (length, character) {
return this.length >= length ? this : (character + this).slice(-length);
};
// Simulated Central Clock
setInterval(() => {
let now = new Date();
let hour = now.getHours().toString().padStart(2, '0');
let min = now.getMinutes().toString().padStart(2, '0');
document.getElementById('txt-app-time').innerText = `${hour}:${min}`;
}, 1000);
</script>
</body>
</html>