Download index.html from smoothoperators/Silverstone_crowd_optimiser: direct link, hf CLI and curl.
- Browser
- Download file 30.2 kB
-
https://huggingface.co/spaces/smoothoperators/Silverstone_crowd_optimiser/resolve/main/index.html
- Command line
-
hf download hf://spaces/smoothoperators/Silverstone_crowd_optimiser/index.html
-
curl -L -o index.html https://huggingface.co/spaces/smoothoperators/Silverstone_crowd_optimiser/resolve/main/index.html
30.2 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Silverstone British GP β Crowd Flow Optimiser</title> | |
| <style> | |
| /* ================================================================== */ | |
| /* 1. STYLING & DASHBOARD LAYOUT (CSS) */ | |
| /* ================================================================== */ | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| body { | |
| background-color: #121212; | |
| color: #ffffff; | |
| padding: 20px; | |
| } | |
| header { | |
| text-align: center; | |
| margin-bottom: 20px; | |
| } | |
| header h1 { | |
| color: #e10600; /* F1 Red Color */ | |
| font-size: 24px; | |
| margin-bottom: 5px; | |
| } | |
| header p { | |
| color: #aaa; | |
| font-size: 14px; | |
| } | |
| /* Two-column responsive grid layout */ | |
| .dashboard { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 20px; | |
| max-width: 1450px; | |
| margin: 0 auto; | |
| } | |
| /* Left Panel: Sliders & Controls */ | |
| .controls-panel { | |
| flex: 1; | |
| min-width: 320px; | |
| background-color: #1e1e1e; | |
| padding: 20px; | |
| border-radius: 10px; | |
| border: 1px solid #333; | |
| } | |
| .controls-panel h2 { | |
| font-size: 18px; | |
| margin-bottom: 15px; | |
| color: #00cc66; | |
| } | |
| .slider-group { | |
| margin-bottom: 15px; | |
| } | |
| .slider-group label { | |
| display: block; | |
| font-size: 13px; | |
| margin-bottom: 6px; | |
| color: #ddd; | |
| } | |
| .slider-group input[type="range"] { | |
| width: 100%; | |
| cursor: pointer; | |
| } | |
| .value-badge { | |
| color: #e10600; | |
| font-weight: bold; | |
| } | |
| /* Online Ordering Section inside Controls Panel */ | |
| .order-btn { | |
| width: 100%; | |
| padding: 12px; | |
| background-color: #e10600; | |
| color: white; | |
| border: none; | |
| border-radius: 6px; | |
| font-weight: bold; | |
| font-size: 14px; | |
| cursor: pointer; | |
| margin-top: 10px; | |
| transition: background 0.2s; | |
| } | |
| .order-btn:hover { | |
| background-color: #ff1e1e; | |
| } | |
| .active-orders-list { | |
| margin-top: 15px; | |
| max-height: 180px; | |
| overflow-y: auto; | |
| background: #141414; | |
| padding: 10px; | |
| border-radius: 6px; | |
| border: 1px solid #333; | |
| } | |
| .order-card { | |
| background: #252525; | |
| padding: 8px; | |
| margin-bottom: 8px; | |
| border-radius: 4px; | |
| border-left: 3px solid #f39c12; | |
| font-size: 12px; | |
| } | |
| .order-card.ready { | |
| border-left-color: #00cc66; | |
| } | |
| /* Modal Overlay */ | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| z-index: 1000; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.8); | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .modal-content { | |
| background-color: #1e1e1e; | |
| padding: 25px; | |
| border-radius: 10px; | |
| border: 1px solid #444; | |
| width: 400px; | |
| max-width: 90%; | |
| } | |
| .modal-content h3 { | |
| color: #e10600; | |
| margin-bottom: 15px; | |
| } | |
| .form-group { | |
| margin-bottom: 12px; | |
| } | |
| .form-group label { | |
| display: block; | |
| font-size: 13px; | |
| margin-bottom: 5px; | |
| color: #ccc; | |
| } | |
| .form-group select, .form-group input { | |
| width: 100%; | |
| padding: 8px; | |
| background: #2b2b2b; | |
| border: 1px solid #444; | |
| color: white; | |
| border-radius: 4px; | |
| } | |
| .modal-actions { | |
| display: flex; | |
| gap: 10px; | |
| margin-top: 20px; | |
| } | |
| .btn-submit { | |
| flex: 1; | |
| background: #00cc66; | |
| color: black; | |
| padding: 10px; | |
| border: none; | |
| border-radius: 4px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| } | |
| .btn-cancel { | |
| flex: 1; | |
| background: #444; | |
| color: white; | |
| padding: 10px; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| /* Right Panel: Interactive Canvas Map & Report */ | |
| .map-panel { | |
| flex: 2; | |
| min-width: 450px; | |
| background-color: #1e1e1e; | |
| padding: 20px; | |
| border-radius: 10px; | |
| border: 1px solid #333; | |
| } | |
| canvas { | |
| width: 100%; | |
| height: auto; | |
| background-color: #181818; | |
| border-radius: 8px; | |
| border: 1px solid #444; | |
| display: block; | |
| } | |
| /* Report Box Styling */ | |
| .report-box { | |
| margin-top: 15px; | |
| padding: 15px; | |
| background-color: #252525; | |
| border-radius: 8px; | |
| border-left: 5px solid #00cc66; | |
| font-size: 14px; | |
| line-height: 1.5; | |
| } | |
| .report-box.alert { | |
| border-left-color: #ff4444; | |
| background-color: #2a1a1a; | |
| } | |
| .report-box h3 { | |
| margin-bottom: 8px; | |
| font-size: 16px; | |
| } | |
| .badge { | |
| display: inline-block; | |
| padding: 3px 8px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| font-weight: bold; | |
| } | |
| .badge-red { background-color: #ff4444; color: white; } | |
| .badge-green { background-color: #00cc66; color: black; } | |
| .badge-purple { background-color: #a020f0; color: white; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>ποΈ SILVERSTONE BRITISH GP β CROWD FLOW OPTIMISER</h1> | |
| <p>Real-time interconnected crowd routing, dynamic bottleneck propagation & online ordering integration</p> | |
| </header> | |
| <div class="dashboard"> | |
| <!-- ============================================================== --> | |
| <!-- LEFT PANEL: USER SLIDER INPUTS & ONLINE ORDERING --> | |
| <!-- ============================================================== --> | |
| <div class="controls-panel"> | |
| <h2>βοΈ Grandstand Crowd Inputs</h2> | |
| <div class="slider-group"> | |
| <label for="hamilton">Hamilton Straight GS: <span id="hamilton-val" class="value-badge">65</span> fans/min</label> | |
| <input type="range" id="hamilton" min="10" max="100" value="65" oninput="updateSimulation()"> | |
| </div> | |
| <div class="slider-group"> | |
| <label for="becketts">Becketts GS: <span id="becketts-val" class="value-badge">60</span> fans/min</label> | |
| <input type="range" id="becketts" min="10" max="100" value="60" oninput="updateSimulation()"> | |
| </div> | |
| <div class="slider-group"> | |
| <label for="stowe">Stowe GS: <span id="stowe-val" class="value-badge">40</span> fans/min</label> | |
| <input type="range" id="stowe" min="10" max="100" value="40" oninput="updateSimulation()"> | |
| </div> | |
| <div class="slider-group"> | |
| <label for="village">Village GS: <span id="village-val" class="value-badge">55</span> fans/min</label> | |
| <input type="range" id="village" min="10" max="100" value="55" oninput="updateSimulation()"> | |
| </div> | |
| <div class="slider-group"> | |
| <label for="club">Club GS: <span id="club-val" class="value-badge">50</span> fans/min</label> | |
| <input type="range" id="club" min="10" max="100" value="50" oninput="updateSimulation()"> | |
| </div> | |
| <hr style="border-color: #333; margin: 15px 0;"> | |
| <!-- ONLINE ORDERING SECTION --> | |
| <h2 style="color: #f39c12;">π Click & Collect Food and Merch Express</h2> | |
| <button class="order-btn" onclick="openOrderModal()">π Place Express Food/Merch Order</button> | |
| <div style="margin-top: 15px;"> | |
| <span style="font-size: 13px; font-weight: bold; color: #aaa;">Active Mobile Orders:</span> | |
| <div id="ordersContainer" class="active-orders-list"> | |
| <p style="color: #666; font-size: 12px; text-align: center;">No active mobile orders</p> | |
| </div> | |
| </div> | |
| <hr style="border-color: #333; margin: 15px 0;"> | |
| <div style="font-size: 12px; color: #aaa; line-height: 1.4;"> | |
| <p><strong>π‘ Dynamic Flow Rules:</strong></p> | |
| <p>β’ <strong>Gate 1 Threshold:</strong> 50 fans/min</p> | |
| <p>β’ <strong>Gate 3 Threshold:</strong> 45 fans/min</p> | |
| <p>β’ Bottlenecks cause cascading <span class="badge badge-red">RED</span> corridor congestions.</p> | |
| <p>β’ Active rerouting renders complex multi-node <span class="badge badge-purple">PURPLE</span> Google Maps detours.</p> | |
| </div> | |
| </div> | |
| <!-- ============================================================== --> | |
| <!-- RIGHT PANEL: HTML5 CANVAS MAP & TEXT ANALYSIS REPORT --> | |
| <!-- ============================================================== --> | |
| <div class="map-panel"> | |
| <canvas id="silverstoneCanvas" width="900" height="520"></canvas> | |
| <div id="reportBox" class="report-box"> | |
| <!-- Dynamic Text Report Injected Here --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ONLINE ORDER MODAL DIALOG --> | |
| <div id="orderModal" class="modal"> | |
| <div class="modal-content"> | |
| <h3>π Place Online Pickup Order</h3> | |
| <div class="form-group"> | |
| <label for="pickupLocation">Select Pickup Location:</label> | |
| <select id="pickupLocation"> | |
| <option value="Main Fan Zone">Main Fan Zone</option> | |
| <option value="Becketts Food">Becketts Food</option> | |
| <option value="Village Plaza">Village Plaza</option> | |
| <option value="Club Eats">Club Eats</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="itemSelect">Select Express Menu Item:</label> | |
| <select id="itemSelect"> | |
| <option value="F1 Burger & Chips Combo (Β£14.00)">F1 Burger & Chips Combo (Β£14.00)</option> | |
| <option value="Woodfired Pizza Slices (Β£11.50)">Woodfired Pizza Slices (Β£11.50)</option> | |
| <option value="Grid Soft Drink & Fries (Β£7.00)">Grid Soft Drink & Fries (Β£7.00)</option> | |
| <option value="Silverstone Official Cap (Β£30.00)">Silverstone Official Cap (Β£30.00)</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="custName">Customer Name / Ticket ID:</label> | |
| <input type="text" id="custName" placeholder="e.g. John - Row C" value="Seat 14B"> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="btn-submit" onclick="submitOrder()">Confirm Order</button> | |
| <button class="btn-cancel" onclick="closeOrderModal()">Cancel</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ================================================================== --> | |
| <!-- 2. JAVASCRIPT SIMULATION & CANVAS DRAWING LOGIC --> | |
| <!-- ================================================================== --> | |
| <script> | |
| // Gate Capacity Limits | |
| const GATE_1_CAPACITY = 50; | |
| const GATE_3_CAPACITY = 45; | |
| // Active Orders State Management | |
| let activeOrders = []; | |
| let orderCounter = 101; | |
| function updateSimulation() { | |
| // 1. READ SLIDER VALUES FROM HTML INPUTS | |
| const hamiltonCrowd = parseInt(document.getElementById('hamilton').value); | |
| const beckettsCrowd = parseInt(document.getElementById('becketts').value); | |
| const stoweCrowd = parseInt(document.getElementById('stowe').value); | |
| const villageCrowd = parseInt(document.getElementById('village').value); | |
| const clubCrowd = parseInt(document.getElementById('club').value); | |
| // Update UI slider text badges | |
| document.getElementById('hamilton-val').innerText = hamiltonCrowd; | |
| document.getElementById('becketts-val').innerText = beckettsCrowd; | |
| document.getElementById('stowe-val').innerText = stoweCrowd; | |
| document.getElementById('village-val').innerText = villageCrowd; | |
| document.getElementById('club-val').innerText = clubCrowd; | |
| // 2. MATHEMATICAL TRAFFIC LOAD CALCULATIONS | |
| const gate1Load = Math.round(hamiltonCrowd * 0.5 + beckettsCrowd * 0.3 + villageCrowd * 0.3); | |
| const gate2Load = Math.round(hamiltonCrowd * 0.2 + clubCrowd * 0.4); | |
| const gate3Load = Math.round(stoweCrowd * 0.6 + beckettsCrowd * 0.4); | |
| const gate4Load = Math.round(villageCrowd * 0.4 + hamiltonCrowd * 0.2); | |
| // Bottleneck flags | |
| const isGate1Bottleneck = gate1Load > GATE_1_CAPACITY; | |
| const isGate3Bottleneck = gate3Load > GATE_3_CAPACITY; | |
| const excessGate1 = isGate1Bottleneck ? (gate1Load - GATE_1_CAPACITY) : 0; | |
| const excessGate3 = isGate3Bottleneck ? (gate3Load - GATE_3_CAPACITY) : 0; | |
| // 3. DRAW MAP ON HTML5 CANVAS | |
| drawCanvasMap( | |
| hamiltonCrowd, beckettsCrowd, stoweCrowd, villageCrowd, clubCrowd, | |
| gate1Load, gate3Load, isGate1Bottleneck, isGate3Bottleneck | |
| ); | |
| // 4. UPDATE TEXT REPORT | |
| updateReport(gate1Load, gate2Load, gate3Load, gate4Load, isGate1Bottleneck, isGate3Bottleneck, excessGate1, excessGate3); | |
| } | |
| /* --- ONLINE ORDERING FUNCTIONS --- */ | |
| function openOrderModal() { | |
| document.getElementById('orderModal').style.display = 'flex'; | |
| } | |
| function closeOrderModal() { | |
| document.getElementById('orderModal').style.display = 'none'; | |
| } | |
| function submitOrder() { | |
| const location = document.getElementById('pickupLocation').value; | |
| const item = document.getElementById('itemSelect').value; | |
| const customer = document.getElementById('custName').value || 'Guest'; | |
| const newOrder = { | |
| id: `#ORD-${orderCounter++}`, | |
| location: location, | |
| item: item, | |
| customer: customer, | |
| status: 'Preparing' | |
| }; | |
| activeOrders.push(newOrder); | |
| closeOrderModal(); | |
| renderOrdersUI(); | |
| updateSimulation(); // Refresh Canvas Map Badges | |
| // Automatically transition order status to Ready after 8 seconds | |
| setTimeout(() => { | |
| newOrder.status = 'Ready for Pickup'; | |
| renderOrdersUI(); | |
| updateSimulation(); | |
| }, 8000); | |
| } | |
| function completeOrder(orderId) { | |
| activeOrders = activeOrders.filter(o => o.id !== orderId); | |
| renderOrdersUI(); | |
| updateSimulation(); | |
| } | |
| function renderOrdersUI() { | |
| const container = document.getElementById('ordersContainer'); | |
| if (activeOrders.length === 0) { | |
| container.innerHTML = `<p style="color: #666; font-size: 12px; text-align: center;">No active mobile orders</p>`; | |
| return; | |
| } | |
| container.innerHTML = activeOrders.map(o => ` | |
| <div class="order-card ${o.status === 'Ready for Pickup' ? 'ready' : ''}"> | |
| <div style="display:flex; justify-space-between; font-weight:bold;"> | |
| <span>${o.id} - ${o.customer}</span> | |
| <span style="color: ${o.status === 'Ready for Pickup' ? '#00cc66' : '#f39c12'}">${o.status}</span> | |
| </div> | |
| <div style="color: #aaa; margin-top:3px;">π ${o.location}</div> | |
| <div style="color: #ddd;">π ${o.item}</div> | |
| ${o.status === 'Ready for Pickup' ? `<button onclick="completeOrder('${o.id}')" style="margin-top:5px; background:#00cc66; color:black; border:none; padding:2px 6px; border-radius:3px; cursor:pointer; font-size:10px; font-weight:bold;">Collect Order</button>` : ''} | |
| </div> | |
| `).join(''); | |
| } | |
| function drawCanvasMap(hamilton, becketts, stowe, village, club, gate1Load, gate3Load, isGate1Bottleneck, isGate3Bottleneck) { | |
| const canvas = document.getElementById('silverstoneCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // Clear canvas background | |
| ctx.fillStyle = '#181818'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Map coordinate converter | |
| function toCanvas(x, y) { | |
| return { | |
| x: x * 65 + 430, | |
| y: -y * 48 + 260 | |
| }; | |
| } | |
| // Helper function to draw multi-point paths | |
| function drawPath(points, color, isDashed = false) { | |
| ctx.beginPath(); | |
| if (isDashed) ctx.setLineDash([8, 6]); | |
| points.forEach((ptKey, idx) => { | |
| const pos = typeof ptKey === 'string' ? nodes[ptKey].pos : toCanvas(ptKey.x, ptKey.y); | |
| if (idx === 0) ctx.moveTo(pos.x, pos.y); | |
| else ctx.lineTo(pos.x, pos.y); | |
| }); | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = isDashed ? 5 : 4; | |
| ctx.stroke(); | |
| if (isDashed) ctx.setLineDash([]); | |
| } | |
| // ------------------------------------------------------------- | |
| // A. DRAW F1 CIRCUIT TRACK | |
| // ------------------------------------------------------------- | |
| const trackPoints = [ | |
| {x: -1, y: 0}, {x: 1, y: -2}, {x: 3, y: -3}, {x: 2, y: -4}, | |
| {x: -1, y: -3}, {x: -2, y: -1}, {x: -3, y: 1}, {x: -1, y: 3}, | |
| {x: 1, y: 4}, {x: 3, y: 4}, {x: 4, y: 2}, {x: 2, y: 0}, {x: -1, y: 0} | |
| ]; | |
| // Outer track border | |
| ctx.beginPath(); | |
| trackPoints.forEach((pt, idx) => { | |
| const c = toCanvas(pt.x, pt.y); | |
| if (idx === 0) ctx.moveTo(c.x, c.y); | |
| else ctx.lineTo(c.x, c.y); | |
| }); | |
| ctx.strokeStyle = '#333333'; | |
| ctx.lineWidth = 14; | |
| ctx.stroke(); | |
| // Racing line | |
| ctx.beginPath(); | |
| ctx.setLineDash([6, 6]); | |
| trackPoints.forEach((pt, idx) => { | |
| const c = toCanvas(pt.x, pt.y); | |
| if (idx === 0) ctx.moveTo(c.x, c.y); | |
| else ctx.lineTo(c.x, c.y); | |
| }); | |
| ctx.strokeStyle = '#aaaaaa'; | |
| ctx.lineWidth = 1.5; | |
| ctx.stroke(); | |
| ctx.setLineDash([]); // Reset dash | |
| // ------------------------------------------------------------- | |
| // B. DEFINE MAP NODES (Grandstands, Food, Hubs, Gates) | |
| // Shifted safely outside the track boundary | |
| // ------------------------------------------------------------- | |
| const nodes = { | |
| // Grandstands | |
| "Hamilton GS": { pos: toCanvas(-2.2, -2.5), type: "gs", label: `Hamilton (${hamilton})` }, | |
| "Becketts GS": { pos: toCanvas(1, 4.8), type: "gs", label: `Becketts (${becketts})` }, | |
| "Stowe GS": { pos: toCanvas(4.8, 1.5), type: "gs", label: `Stowe (${stowe})` }, | |
| "Village GS": { pos: toCanvas(-4.2, 1.2), type: "gs", label: `Village (${village})` }, | |
| "Club GS": { pos: toCanvas(2.8, -4.5), type: "gs", label: `Club (${club})` }, | |
| // Food Stalls & Arenas | |
| "Main Fan Zone": { pos: toCanvas(-4.2, -1.0), type: "food", label: "π Main Fan Zone" }, | |
| "Becketts Food": { pos: toCanvas(2.8, 4.2), type: "food", label: "π Becketts Food" }, | |
| "Village Plaza": { pos: toCanvas(-3.2, 3.2), type: "food", label: "π Village Plaza" }, | |
| "Club Eats": { pos: toCanvas(-0.5, -4.5), type: "food", label: "π¦ Club Eats" }, | |
| // Intermediate Junction Hubs | |
| "Arena Crossroad": { pos: toCanvas(-3.5, -2.2), type: "hub", label: "π Arena Hub" }, | |
| "Becketts Interchange": { pos: toCanvas(-1.0, 4.2), type: "hub", label: "π Becketts Hub" }, | |
| "South Bypass": { pos: toCanvas(-2.2, -3.8), type: "hub", label: "π South Hub" }, | |
| // Gates | |
| "Main Gate 1": { pos: toCanvas(-4.8, -2.2), type: "gate", label: "πͺ Main Gate 1" }, | |
| "South Gate 2": { pos: toCanvas(1.5, -4.8), type: "gate", label: "πͺ South Gate 2" }, | |
| "East Gate 3": { pos: toCanvas(5.2, -1.5), type: "gate", label: "πͺ East Gate 3" }, | |
| "West Gate 4": { pos: toCanvas(-4.8, 3.2), type: "gate", label: "πͺ West Gate 4" } | |
| }; | |
| // ------------------------------------------------------------- | |
| // C. INTERCONNECTED STANDARD PEDESTRIAN PATHS (GREEN / RED) | |
| // Explicit waypoint routing around track perimeter | |
| // ------------------------------------------------------------- | |
| const pathColorG1 = isGate1Bottleneck ? "#ff4444" : "#00cc66"; | |
| const pathColorG3 = isGate3Bottleneck ? "#ff4444" : "#00cc66"; | |
| // Gate 1 Network | |
| drawPath(["Hamilton GS", "Arena Crossroad"], pathColorG1); | |
| drawPath(["Arena Crossroad", "Main Fan Zone"], pathColorG1); | |
| drawPath(["Main Fan Zone", "Main Gate 1"], pathColorG1); | |
| drawPath(["Village GS", "Main Fan Zone"], pathColorG1); | |
| // Gate 3 & Becketts Network | |
| drawPath(["Becketts GS", "Becketts Interchange"], pathColorG3); | |
| drawPath(["Becketts GS", "Becketts Food"], pathColorG3); | |
| drawPath(["Becketts Food", "Stowe GS"], pathColorG3); | |
| drawPath(["Stowe GS", "East Gate 3"], pathColorG3); | |
| // Outer Northern Connection (Becketts Hub <-> Village Plaza <-> Fan Zone around top/left) | |
| drawPath(["Becketts Interchange", "Village Plaza"], pathColorG1); | |
| drawPath(["Village Plaza", "Village GS"], "#00cc66"); | |
| drawPath(["Village Plaza", "West Gate 4"], "#00cc66"); | |
| // Outer Southern Connection (Hamilton <-> South Bypass <-> Club Eats <-> South Gate 2 around bottom) | |
| drawPath(["Hamilton GS", "South Bypass"], "#00cc66"); | |
| drawPath(["South Bypass", "Club Eats"], "#00cc66"); | |
| drawPath(["Club GS", "Club Eats"], "#00cc66"); | |
| drawPath(["Club Eats", "South Gate 2"], "#00cc66"); | |
| // ------------------------------------------------------------- | |
| // D. EXPANDED GOOGLE MAPS MULTI-NODE DETOURS (PURPLE) | |
| // Detours routed cleanly along the outer perimeter | |
| // ------------------------------------------------------------- | |
| if (isGate1Bottleneck) { | |
| // Fan Zone -> South Hub -> Club Eats -> South Gate 2 | |
| drawPath(["Main Fan Zone", "Arena Crossroad", "South Bypass", "Club Eats", "South Gate 2"], "#a020f0", true); | |
| // Arena Hub -> West Gate 4 | |
| drawPath(["Arena Crossroad", "Village GS", "West Gate 4"], "#a020f0", true); | |
| } | |
| if (isGate3Bottleneck) { | |
| // Becketts Hub -> Village Plaza -> Main Fan Zone -> South Hub -> South Gate 2 | |
| drawPath(["Becketts Interchange", "Village Plaza", "Main Fan Zone", "South Bypass", "South Gate 2"], "#a020f0", true); | |
| } | |
| // ------------------------------------------------------------- | |
| // E. DRAW NODE BOXES, LABELS & ORDER BADGES | |
| // ------------------------------------------------------------- | |
| for (let key in nodes) { | |
| const n = nodes[key]; | |
| ctx.font = "bold 11px Arial"; | |
| const textWidth = ctx.measureText(n.label).width; | |
| const boxPadding = 7; | |
| let boxColor = "#3498db"; // Blue for Grandstands | |
| if (n.type === "food") boxColor = "#f39c12"; // Orange for Food | |
| if (n.type === "hub") boxColor = "#7f8c8d"; // Grey for Hubs | |
| if (n.type === "gate") { | |
| if ((key === "Main Gate 1" && isGate1Bottleneck) || (key === "East Gate 3" && isGate3Bottleneck)) { | |
| boxColor = "#ff4444"; | |
| } else { | |
| boxColor = "#00cc66"; | |
| } | |
| } | |
| // Draw background rounded box | |
| ctx.fillStyle = boxColor; | |
| ctx.beginPath(); | |
| ctx.roundRect( | |
| n.pos.x - textWidth / 2 - boxPadding, | |
| n.pos.y - 12, | |
| textWidth + boxPadding * 2, | |
| 24, | |
| 6 | |
| ); | |
| ctx.fill(); | |
| ctx.strokeStyle = "#ffffff"; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| // Draw label text | |
| ctx.fillStyle = "#ffffff"; | |
| ctx.textAlign = "center"; | |
| ctx.textBaseline = "middle"; | |
| ctx.fillText(n.label, n.pos.x, n.pos.y); | |
| // DRAW PENDING ORDERS OVERLAY BADGE FOR FOOD STALLS | |
| if (n.type === "food") { | |
| const pendingCount = activeOrders.filter(o => o.location === key).length; | |
| if (pendingCount > 0) { | |
| ctx.fillStyle = "#e10600"; | |
| ctx.beginPath(); | |
| ctx.arc(n.pos.x + textWidth / 2 + 5, n.pos.y - 10, 10, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.strokeStyle = "#ffffff"; | |
| ctx.lineWidth = 1.5; | |
| ctx.stroke(); | |
| ctx.fillStyle = "#ffffff"; | |
| ctx.font = "bold 10px Arial"; | |
| ctx.fillText(pendingCount, n.pos.x + textWidth / 2 + 5, n.pos.y - 10); | |
| } | |
| } | |
| } | |
| // Draw Legend | |
| ctx.font = "11px Arial"; | |
| ctx.fillStyle = "#ffffff"; | |
| ctx.textAlign = "left"; | |
| ctx.fillText("π’ Clear Pathway", 20, 25); | |
| ctx.fillText("π΄ Congested Network Segment", 140, 25); | |
| ctx.fillText("π£ Google Maps Active Multi-Leg Detour", 330, 25); | |
| } | |
| function updateReport(gate1, gate2, gate3, gate4, isG1Alert, isG3Alert, excess1, excess3) { | |
| const reportDiv = document.getElementById('reportBox'); | |
| if (isG1Alert || isG3Alert) { | |
| reportDiv.className = "report-box alert"; | |
| let alertsHtml = `<h3 style="color: #ff4444;">π΄ NETWORK BOTTLENECK ALERT ACTIVATED</h3>`; | |
| if (isG1Alert) { | |
| alertsHtml += `<p>β’ <strong>Main Gate 1 Bottleneck:</strong> Load <code>${gate1}</code> fans/min (Max limit: ${GATE_1_CAPACITY}). Overflow: <code>${excess1}</code> fans.</p>`; | |
| } | |
| if (isG3Alert) { | |
| alertsHtml += `<p>β’ <strong>East Gate 3 Bottleneck:</strong> Load <code>${gate3}</code> fans/min (Max limit: ${GATE_3_CAPACITY}). Overflow: <code>${excess3}</code> fans.</p>`; | |
| } | |
| alertsHtml += ` | |
| <hr style="border-color: #552222; margin: 10px 0;"> | |
| <h4 style="color: #a020f0;">π Dynamic Rerouting Protocol:</h4> | |
| <p>β’ Activating interconnected <span class="badge badge-purple">PURPLE DETOURS</span> across <strong>Arena Hub</strong>, <strong>South Bypass</strong>, and <strong>Becketts Interchange</strong>.</p> | |
| <p>β’ Directing overflow pedestrian traffic to under-utilized exit points at <strong>South Gate 2</strong> and <strong>West Gate 4</strong>.</p> | |
| `; | |
| reportDiv.innerHTML = alertsHtml; | |
| } else { | |
| reportDiv.className = "report-box"; | |
| reportDiv.innerHTML = ` | |
| <h3 style="color: #00cc66;">π’ ALL INTERCONNECTED PATHWAYS OPTIMAL</h3> | |
| <p>β’ <strong>Main Gate 1 Load:</strong> <code>${gate1}</code> / ${GATE_1_CAPACITY} fans/min (Safe)</p> | |
| <p>β’ <strong>South Gate 2 Load:</strong> <code>${gate2}</code> / 50 fans/min (Safe)</p> | |
| <p>β’ <strong>East Gate 3 Load:</strong> <code>${gate3}</code> / ${GATE_3_CAPACITY} fans/min (Safe)</p> | |
| <p>β’ <strong>West Gate 4 Load:</strong> <code>${gate4}</code> / 45 fans/min (Safe)</p> | |
| <p style="margin-top: 5px; color: #aaa;"><em>Status: Grid flow is balanced across all junctions. No purple detour routing required.</em></p> | |
| `; | |
| } | |
| } | |
| // Initialize simulation on initial page load | |
| window.onload = updateSimulation; | |
| </script> | |
| </body> | |
| </html> |