Update index.html
Browse files- index.html +73 -112
index.html
CHANGED
|
@@ -261,9 +261,7 @@
|
|
| 261 |
</header>
|
| 262 |
|
| 263 |
<div class="dashboard">
|
| 264 |
-
<!--
|
| 265 |
-
<!-- LEFT PANEL: USER SLIDER INPUTS & ONLINE ORDERING -->
|
| 266 |
-
<!-- ============================================================== -->
|
| 267 |
<div class="controls-panel">
|
| 268 |
<h2>βοΈ Grandstand Crowd Inputs</h2>
|
| 269 |
|
|
@@ -294,7 +292,6 @@
|
|
| 294 |
|
| 295 |
<hr style="border-color: #333; margin: 15px 0;">
|
| 296 |
|
| 297 |
-
<!-- ONLINE ORDERING SECTION -->
|
| 298 |
<h2 style="color: #f39c12;">π Click & Collect Food Express</h2>
|
| 299 |
<button class="order-btn" onclick="openOrderModal()">π Place Express Food/Merch Order</button>
|
| 300 |
|
|
@@ -316,18 +313,14 @@
|
|
| 316 |
</div>
|
| 317 |
</div>
|
| 318 |
|
| 319 |
-
<!--
|
| 320 |
-
<!-- RIGHT PANEL: HTML5 CANVAS MAP & TEXT ANALYSIS REPORT -->
|
| 321 |
-
<!-- ============================================================== -->
|
| 322 |
<div class="map-panel">
|
| 323 |
<canvas id="silverstoneCanvas" width="900" height="520"></canvas>
|
| 324 |
-
<div id="reportBox" class="report-box">
|
| 325 |
-
<!-- Dynamic Text Report Injected Here -->
|
| 326 |
-
</div>
|
| 327 |
</div>
|
| 328 |
</div>
|
| 329 |
|
| 330 |
-
<!--
|
| 331 |
<div id="orderModal" class="modal">
|
| 332 |
<div class="modal-content">
|
| 333 |
<h3>π Place Online Pickup Order</h3>
|
|
@@ -360,63 +353,47 @@
|
|
| 360 |
</div>
|
| 361 |
</div>
|
| 362 |
|
| 363 |
-
<!--
|
| 364 |
-
<!-- 2. JAVASCRIPT SIMULATION & CANVAS DRAWING LOGIC -->
|
| 365 |
-
<!-- ================================================================== -->
|
| 366 |
<script>
|
| 367 |
-
// Gate Capacity Limits
|
| 368 |
const GATE_1_CAPACITY = 50;
|
| 369 |
const GATE_3_CAPACITY = 45;
|
| 370 |
|
| 371 |
-
// Active Orders State Management
|
| 372 |
let activeOrders = [];
|
| 373 |
let orderCounter = 101;
|
| 374 |
|
| 375 |
function updateSimulation() {
|
| 376 |
-
// 1. READ SLIDER VALUES FROM HTML INPUTS
|
| 377 |
const hamiltonCrowd = parseInt(document.getElementById('hamilton').value);
|
| 378 |
const beckettsCrowd = parseInt(document.getElementById('becketts').value);
|
| 379 |
const stoweCrowd = parseInt(document.getElementById('stowe').value);
|
| 380 |
const villageCrowd = parseInt(document.getElementById('village').value);
|
| 381 |
const clubCrowd = parseInt(document.getElementById('club').value);
|
| 382 |
|
| 383 |
-
// Update UI slider text badges
|
| 384 |
document.getElementById('hamilton-val').innerText = hamiltonCrowd;
|
| 385 |
document.getElementById('becketts-val').innerText = beckettsCrowd;
|
| 386 |
document.getElementById('stowe-val').innerText = stoweCrowd;
|
| 387 |
document.getElementById('village-val').innerText = villageCrowd;
|
| 388 |
document.getElementById('club-val').innerText = clubCrowd;
|
| 389 |
|
| 390 |
-
// 2. MATHEMATICAL TRAFFIC LOAD CALCULATIONS
|
| 391 |
const gate1Load = Math.round(hamiltonCrowd * 0.5 + beckettsCrowd * 0.3 + villageCrowd * 0.3);
|
| 392 |
const gate2Load = Math.round(hamiltonCrowd * 0.2 + clubCrowd * 0.4);
|
| 393 |
const gate3Load = Math.round(stoweCrowd * 0.6 + beckettsCrowd * 0.4);
|
| 394 |
const gate4Load = Math.round(villageCrowd * 0.4 + hamiltonCrowd * 0.2);
|
| 395 |
|
| 396 |
-
// Bottleneck flags
|
| 397 |
const isGate1Bottleneck = gate1Load > GATE_1_CAPACITY;
|
| 398 |
const isGate3Bottleneck = gate3Load > GATE_3_CAPACITY;
|
| 399 |
const excessGate1 = isGate1Bottleneck ? (gate1Load - GATE_1_CAPACITY) : 0;
|
| 400 |
const excessGate3 = isGate3Bottleneck ? (gate3Load - GATE_3_CAPACITY) : 0;
|
| 401 |
|
| 402 |
-
// 3. DRAW MAP ON HTML5 CANVAS
|
| 403 |
drawCanvasMap(
|
| 404 |
hamiltonCrowd, beckettsCrowd, stoweCrowd, villageCrowd, clubCrowd,
|
| 405 |
gate1Load, gate3Load, isGate1Bottleneck, isGate3Bottleneck
|
| 406 |
);
|
| 407 |
|
| 408 |
-
// 4. UPDATE TEXT REPORT
|
| 409 |
updateReport(gate1Load, gate2Load, gate3Load, gate4Load, isGate1Bottleneck, isGate3Bottleneck, excessGate1, excessGate3);
|
| 410 |
}
|
| 411 |
|
| 412 |
-
|
| 413 |
-
function
|
| 414 |
-
document.getElementById('orderModal').style.display = 'flex';
|
| 415 |
-
}
|
| 416 |
-
|
| 417 |
-
function closeOrderModal() {
|
| 418 |
-
document.getElementById('orderModal').style.display = 'none';
|
| 419 |
-
}
|
| 420 |
|
| 421 |
function submitOrder() {
|
| 422 |
const location = document.getElementById('pickupLocation').value;
|
|
@@ -434,9 +411,8 @@
|
|
| 434 |
activeOrders.push(newOrder);
|
| 435 |
closeOrderModal();
|
| 436 |
renderOrdersUI();
|
| 437 |
-
updateSimulation();
|
| 438 |
|
| 439 |
-
// Automatically transition order status to Ready after 8 seconds
|
| 440 |
setTimeout(() => {
|
| 441 |
newOrder.status = 'Ready for Pickup';
|
| 442 |
renderOrdersUI();
|
|
@@ -459,7 +435,7 @@
|
|
| 459 |
|
| 460 |
container.innerHTML = activeOrders.map(o => `
|
| 461 |
<div class="order-card ${o.status === 'Ready for Pickup' ? 'ready' : ''}">
|
| 462 |
-
<div style="display:flex; justify-space-between; font-weight:bold;">
|
| 463 |
<span>${o.id} - ${o.customer}</span>
|
| 464 |
<span style="color: ${o.status === 'Ready for Pickup' ? '#00cc66' : '#f39c12'}">${o.status}</span>
|
| 465 |
</div>
|
|
@@ -474,11 +450,10 @@
|
|
| 474 |
const canvas = document.getElementById('silverstoneCanvas');
|
| 475 |
const ctx = canvas.getContext('2d');
|
| 476 |
|
| 477 |
-
// Clear canvas background
|
| 478 |
ctx.fillStyle = '#181818';
|
| 479 |
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| 480 |
|
| 481 |
-
//
|
| 482 |
function toCanvas(x, y) {
|
| 483 |
return {
|
| 484 |
x: x * 65 + 430,
|
|
@@ -486,28 +461,23 @@
|
|
| 486 |
};
|
| 487 |
}
|
| 488 |
|
| 489 |
-
//
|
| 490 |
-
function drawPath(points, color, isDashed = false) {
|
| 491 |
-
ctx.beginPath();
|
| 492 |
-
if (isDashed) ctx.setLineDash([8, 6]);
|
| 493 |
-
points.forEach((ptKey, idx) => {
|
| 494 |
-
const pos = typeof ptKey === 'string' ? nodes[ptKey].pos : toCanvas(ptKey.x, ptKey.y);
|
| 495 |
-
if (idx === 0) ctx.moveTo(pos.x, pos.y);
|
| 496 |
-
else ctx.lineTo(pos.x, pos.y);
|
| 497 |
-
});
|
| 498 |
-
ctx.strokeStyle = color;
|
| 499 |
-
ctx.lineWidth = isDashed ? 5 : 4;
|
| 500 |
-
ctx.stroke();
|
| 501 |
-
if (isDashed) ctx.setLineDash([]);
|
| 502 |
-
}
|
| 503 |
-
|
| 504 |
-
// -------------------------------------------------------------
|
| 505 |
-
// A. DRAW F1 CIRCUIT TRACK
|
| 506 |
-
// -------------------------------------------------------------
|
| 507 |
const trackPoints = [
|
| 508 |
-
{x: -1, y:
|
| 509 |
-
{x: -
|
| 510 |
-
{x:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
];
|
| 512 |
|
| 513 |
// Outer track border
|
|
@@ -532,97 +502,92 @@
|
|
| 532 |
ctx.strokeStyle = '#aaaaaa';
|
| 533 |
ctx.lineWidth = 1.5;
|
| 534 |
ctx.stroke();
|
| 535 |
-
ctx.setLineDash([]);
|
| 536 |
|
| 537 |
-
//
|
| 538 |
-
// B. DEFINE MAP NODES (Grandstands, Food, Hubs, Gates)
|
| 539 |
-
// Shifted safely outside the track boundary
|
| 540 |
-
// -------------------------------------------------------------
|
| 541 |
const nodes = {
|
| 542 |
// Grandstands
|
| 543 |
-
"Hamilton GS": { pos: toCanvas(-
|
| 544 |
-
"Becketts GS": { pos: toCanvas(
|
| 545 |
-
"Stowe GS": { pos: toCanvas(4.8, 1.
|
| 546 |
-
"Village GS": { pos: toCanvas(-
|
| 547 |
-
"Club GS": { pos: toCanvas(
|
| 548 |
|
| 549 |
// Food Stalls & Arenas
|
| 550 |
-
"Main Fan Zone": { pos: toCanvas(-
|
| 551 |
-
"Becketts Food": { pos: toCanvas(2.
|
| 552 |
-
"Village Plaza": { pos: toCanvas(-3.2,
|
| 553 |
-
"Club Eats": { pos: toCanvas(-
|
| 554 |
|
| 555 |
// Intermediate Junction Hubs
|
| 556 |
-
"Arena Crossroad": { pos: toCanvas(-3.
|
| 557 |
-
"Becketts Interchange": { pos: toCanvas(
|
| 558 |
-
"South Bypass": { pos: toCanvas(-
|
| 559 |
-
|
| 560 |
-
// Gates
|
| 561 |
-
"Main Gate 1": { pos: toCanvas(-4.8, -2.
|
| 562 |
-
"South Gate 2": { pos: toCanvas(
|
| 563 |
-
"East Gate 3": { pos: toCanvas(5.2,
|
| 564 |
-
"West Gate 4": { pos: toCanvas(-4.
|
| 565 |
};
|
| 566 |
|
| 567 |
-
//
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
const pathColorG1 = isGate1Bottleneck ? "#ff4444" : "#00cc66";
|
| 572 |
const pathColorG3 = isGate3Bottleneck ? "#ff4444" : "#00cc66";
|
| 573 |
|
| 574 |
-
//
|
| 575 |
drawPath(["Hamilton GS", "Arena Crossroad"], pathColorG1);
|
| 576 |
drawPath(["Arena Crossroad", "Main Fan Zone"], pathColorG1);
|
| 577 |
drawPath(["Main Fan Zone", "Main Gate 1"], pathColorG1);
|
| 578 |
-
drawPath(["Village GS",
|
| 579 |
|
| 580 |
-
// Gate 3 & Becketts Network
|
| 581 |
drawPath(["Becketts GS", "Becketts Interchange"], pathColorG3);
|
| 582 |
drawPath(["Becketts GS", "Becketts Food"], pathColorG3);
|
| 583 |
drawPath(["Becketts Food", "Stowe GS"], pathColorG3);
|
| 584 |
drawPath(["Stowe GS", "East Gate 3"], pathColorG3);
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
drawPath(["Becketts Interchange", "Village Plaza"], pathColorG1);
|
| 588 |
-
drawPath(["Village Plaza", "Village GS"], "#00cc66");
|
| 589 |
drawPath(["Village Plaza", "West Gate 4"], "#00cc66");
|
| 590 |
|
| 591 |
-
// Outer Southern Connection (Hamilton <-> South Bypass <-> Club Eats <-> South Gate 2 around bottom)
|
| 592 |
drawPath(["Hamilton GS", "South Bypass"], "#00cc66");
|
| 593 |
drawPath(["South Bypass", "Club Eats"], "#00cc66");
|
| 594 |
drawPath(["Club GS", "Club Eats"], "#00cc66");
|
| 595 |
drawPath(["Club Eats", "South Gate 2"], "#00cc66");
|
| 596 |
|
| 597 |
-
//
|
| 598 |
-
// D. EXPANDED GOOGLE MAPS MULTI-NODE DETOURS (PURPLE)
|
| 599 |
-
// Detours routed cleanly along the outer perimeter
|
| 600 |
-
// -------------------------------------------------------------
|
| 601 |
if (isGate1Bottleneck) {
|
| 602 |
-
// Fan Zone -> South Hub -> Club Eats -> South Gate 2
|
| 603 |
drawPath(["Main Fan Zone", "Arena Crossroad", "South Bypass", "Club Eats", "South Gate 2"], "#a020f0", true);
|
| 604 |
-
|
| 605 |
-
// Arena Hub -> West Gate 4
|
| 606 |
-
drawPath(["Arena Crossroad", "Village GS", "West Gate 4"], "#a020f0", true);
|
| 607 |
}
|
| 608 |
|
| 609 |
if (isGate3Bottleneck) {
|
| 610 |
-
|
| 611 |
-
drawPath(["Becketts Interchange", "Village Plaza", "Main Fan Zone", "South Bypass", "South Gate 2"], "#a020f0", true);
|
| 612 |
}
|
| 613 |
|
| 614 |
-
//
|
| 615 |
-
// E. DRAW NODE BOXES, LABELS & ORDER BADGES
|
| 616 |
-
// -------------------------------------------------------------
|
| 617 |
for (let key in nodes) {
|
| 618 |
const n = nodes[key];
|
| 619 |
ctx.font = "bold 11px Arial";
|
| 620 |
const textWidth = ctx.measureText(n.label).width;
|
| 621 |
const boxPadding = 7;
|
| 622 |
|
| 623 |
-
let boxColor = "#3498db";
|
| 624 |
-
if (n.type === "food") boxColor = "#f39c12";
|
| 625 |
-
if (n.type === "hub") boxColor = "#7f8c8d";
|
| 626 |
if (n.type === "gate") {
|
| 627 |
if ((key === "Main Gate 1" && isGate1Bottleneck) || (key === "East Gate 3" && isGate3Bottleneck)) {
|
| 628 |
boxColor = "#ff4444";
|
|
@@ -631,7 +596,6 @@
|
|
| 631 |
}
|
| 632 |
}
|
| 633 |
|
| 634 |
-
// Draw background rounded box
|
| 635 |
ctx.fillStyle = boxColor;
|
| 636 |
ctx.beginPath();
|
| 637 |
ctx.roundRect(
|
|
@@ -646,13 +610,11 @@
|
|
| 646 |
ctx.lineWidth = 1;
|
| 647 |
ctx.stroke();
|
| 648 |
|
| 649 |
-
// Draw label text
|
| 650 |
ctx.fillStyle = "#ffffff";
|
| 651 |
ctx.textAlign = "center";
|
| 652 |
ctx.textBaseline = "middle";
|
| 653 |
ctx.fillText(n.label, n.pos.x, n.pos.y);
|
| 654 |
|
| 655 |
-
// DRAW PENDING ORDERS OVERLAY BADGE FOR FOOD STALLS
|
| 656 |
if (n.type === "food") {
|
| 657 |
const pendingCount = activeOrders.filter(o => o.location === key).length;
|
| 658 |
if (pendingCount > 0) {
|
|
@@ -671,7 +633,7 @@
|
|
| 671 |
}
|
| 672 |
}
|
| 673 |
|
| 674 |
-
//
|
| 675 |
ctx.font = "11px Arial";
|
| 676 |
ctx.fillStyle = "#ffffff";
|
| 677 |
ctx.textAlign = "left";
|
|
@@ -714,7 +676,6 @@
|
|
| 714 |
}
|
| 715 |
}
|
| 716 |
|
| 717 |
-
// Initialize simulation on initial page load
|
| 718 |
window.onload = updateSimulation;
|
| 719 |
</script>
|
| 720 |
</body>
|
|
|
|
| 261 |
</header>
|
| 262 |
|
| 263 |
<div class="dashboard">
|
| 264 |
+
<!-- LEFT PANEL -->
|
|
|
|
|
|
|
| 265 |
<div class="controls-panel">
|
| 266 |
<h2>βοΈ Grandstand Crowd Inputs</h2>
|
| 267 |
|
|
|
|
| 292 |
|
| 293 |
<hr style="border-color: #333; margin: 15px 0;">
|
| 294 |
|
|
|
|
| 295 |
<h2 style="color: #f39c12;">π Click & Collect Food Express</h2>
|
| 296 |
<button class="order-btn" onclick="openOrderModal()">π Place Express Food/Merch Order</button>
|
| 297 |
|
|
|
|
| 313 |
</div>
|
| 314 |
</div>
|
| 315 |
|
| 316 |
+
<!-- RIGHT PANEL -->
|
|
|
|
|
|
|
| 317 |
<div class="map-panel">
|
| 318 |
<canvas id="silverstoneCanvas" width="900" height="520"></canvas>
|
| 319 |
+
<div id="reportBox" class="report-box"></div>
|
|
|
|
|
|
|
| 320 |
</div>
|
| 321 |
</div>
|
| 322 |
|
| 323 |
+
<!-- MODAL -->
|
| 324 |
<div id="orderModal" class="modal">
|
| 325 |
<div class="modal-content">
|
| 326 |
<h3>π Place Online Pickup Order</h3>
|
|
|
|
| 353 |
</div>
|
| 354 |
</div>
|
| 355 |
|
| 356 |
+
<!-- SCRIPT -->
|
|
|
|
|
|
|
| 357 |
<script>
|
|
|
|
| 358 |
const GATE_1_CAPACITY = 50;
|
| 359 |
const GATE_3_CAPACITY = 45;
|
| 360 |
|
|
|
|
| 361 |
let activeOrders = [];
|
| 362 |
let orderCounter = 101;
|
| 363 |
|
| 364 |
function updateSimulation() {
|
|
|
|
| 365 |
const hamiltonCrowd = parseInt(document.getElementById('hamilton').value);
|
| 366 |
const beckettsCrowd = parseInt(document.getElementById('becketts').value);
|
| 367 |
const stoweCrowd = parseInt(document.getElementById('stowe').value);
|
| 368 |
const villageCrowd = parseInt(document.getElementById('village').value);
|
| 369 |
const clubCrowd = parseInt(document.getElementById('club').value);
|
| 370 |
|
|
|
|
| 371 |
document.getElementById('hamilton-val').innerText = hamiltonCrowd;
|
| 372 |
document.getElementById('becketts-val').innerText = beckettsCrowd;
|
| 373 |
document.getElementById('stowe-val').innerText = stoweCrowd;
|
| 374 |
document.getElementById('village-val').innerText = villageCrowd;
|
| 375 |
document.getElementById('club-val').innerText = clubCrowd;
|
| 376 |
|
|
|
|
| 377 |
const gate1Load = Math.round(hamiltonCrowd * 0.5 + beckettsCrowd * 0.3 + villageCrowd * 0.3);
|
| 378 |
const gate2Load = Math.round(hamiltonCrowd * 0.2 + clubCrowd * 0.4);
|
| 379 |
const gate3Load = Math.round(stoweCrowd * 0.6 + beckettsCrowd * 0.4);
|
| 380 |
const gate4Load = Math.round(villageCrowd * 0.4 + hamiltonCrowd * 0.2);
|
| 381 |
|
|
|
|
| 382 |
const isGate1Bottleneck = gate1Load > GATE_1_CAPACITY;
|
| 383 |
const isGate3Bottleneck = gate3Load > GATE_3_CAPACITY;
|
| 384 |
const excessGate1 = isGate1Bottleneck ? (gate1Load - GATE_1_CAPACITY) : 0;
|
| 385 |
const excessGate3 = isGate3Bottleneck ? (gate3Load - GATE_3_CAPACITY) : 0;
|
| 386 |
|
|
|
|
| 387 |
drawCanvasMap(
|
| 388 |
hamiltonCrowd, beckettsCrowd, stoweCrowd, villageCrowd, clubCrowd,
|
| 389 |
gate1Load, gate3Load, isGate1Bottleneck, isGate3Bottleneck
|
| 390 |
);
|
| 391 |
|
|
|
|
| 392 |
updateReport(gate1Load, gate2Load, gate3Load, gate4Load, isGate1Bottleneck, isGate3Bottleneck, excessGate1, excessGate3);
|
| 393 |
}
|
| 394 |
|
| 395 |
+
function openOrderModal() { document.getElementById('orderModal').style.display = 'flex'; }
|
| 396 |
+
function closeOrderModal() { document.getElementById('orderModal').style.display = 'none'; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
|
| 398 |
function submitOrder() {
|
| 399 |
const location = document.getElementById('pickupLocation').value;
|
|
|
|
| 411 |
activeOrders.push(newOrder);
|
| 412 |
closeOrderModal();
|
| 413 |
renderOrdersUI();
|
| 414 |
+
updateSimulation();
|
| 415 |
|
|
|
|
| 416 |
setTimeout(() => {
|
| 417 |
newOrder.status = 'Ready for Pickup';
|
| 418 |
renderOrdersUI();
|
|
|
|
| 435 |
|
| 436 |
container.innerHTML = activeOrders.map(o => `
|
| 437 |
<div class="order-card ${o.status === 'Ready for Pickup' ? 'ready' : ''}">
|
| 438 |
+
<div style="display:flex; justify-content:space-between; font-weight:bold;">
|
| 439 |
<span>${o.id} - ${o.customer}</span>
|
| 440 |
<span style="color: ${o.status === 'Ready for Pickup' ? '#00cc66' : '#f39c12'}">${o.status}</span>
|
| 441 |
</div>
|
|
|
|
| 450 |
const canvas = document.getElementById('silverstoneCanvas');
|
| 451 |
const ctx = canvas.getContext('2d');
|
| 452 |
|
|
|
|
| 453 |
ctx.fillStyle = '#181818';
|
| 454 |
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| 455 |
|
| 456 |
+
// Coordinate system mapping
|
| 457 |
function toCanvas(x, y) {
|
| 458 |
return {
|
| 459 |
x: x * 65 + 430,
|
|
|
|
| 461 |
};
|
| 462 |
}
|
| 463 |
|
| 464 |
+
// Real Silverstone circuit geometry outline
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
const trackPoints = [
|
| 466 |
+
{x: -1.5, y: -2.5}, // Hamilton Straight / Grid
|
| 467 |
+
{x: -0.2, y: -2.5}, // Abbey
|
| 468 |
+
{x: 0.5, y: -1.8}, // Farm Curve
|
| 469 |
+
{x: -0.2, y: -1.0}, // Village Loop
|
| 470 |
+
{x: -1.2, y: -1.2}, // The Loop
|
| 471 |
+
{x: -2.2, y: 0.2}, // Wellington Straight
|
| 472 |
+
{x: -2.0, y: 1.8}, // Brooklands / Luffield
|
| 473 |
+
{x: -0.8, y: 2.2}, // Woodcote
|
| 474 |
+
{x: 1.2, y: 3.2}, // Copse
|
| 475 |
+
{x: 3.2, y: 2.8}, // Maggotts / Becketts
|
| 476 |
+
{x: 3.8, y: 1.2}, // Chapel / Hangar Straight
|
| 477 |
+
{x: 3.5, y: -1.2}, // Stowe
|
| 478 |
+
{x: 2.2, y: -3.0}, // Vale
|
| 479 |
+
{x: 0.2, y: -3.2}, // Club
|
| 480 |
+
{x: -1.5, y: -2.5} // Finish Line / Main Straight
|
| 481 |
];
|
| 482 |
|
| 483 |
// Outer track border
|
|
|
|
| 502 |
ctx.strokeStyle = '#aaaaaa';
|
| 503 |
ctx.lineWidth = 1.5;
|
| 504 |
ctx.stroke();
|
| 505 |
+
ctx.setLineDash([]);
|
| 506 |
|
| 507 |
+
// MAP NODES positioned outside the track outline
|
|
|
|
|
|
|
|
|
|
| 508 |
const nodes = {
|
| 509 |
// Grandstands
|
| 510 |
+
"Hamilton GS": { pos: toCanvas(-1.0, -3.4), type: "gs", label: `Hamilton (${hamilton})` },
|
| 511 |
+
"Becketts GS": { pos: toCanvas(3.8, 3.5), type: "gs", label: `Becketts (${becketts})` },
|
| 512 |
+
"Stowe GS": { pos: toCanvas(4.8, -1.2), type: "gs", label: `Stowe (${stowe})` },
|
| 513 |
+
"Village GS": { pos: toCanvas(-0.2, -0.2), type: "gs", label: `Village (${village})` },
|
| 514 |
+
"Club GS": { pos: toCanvas(0.5, -4.2), type: "gs", label: `Club (${club})` },
|
| 515 |
|
| 516 |
// Food Stalls & Arenas
|
| 517 |
+
"Main Fan Zone": { pos: toCanvas(-2.8, -1.8), type: "food", label: "π Main Fan Zone" },
|
| 518 |
+
"Becketts Food": { pos: toCanvas(2.2, 4.0), type: "food", label: "π Becketts Food" },
|
| 519 |
+
"Village Plaza": { pos: toCanvas(-3.2, 1.2), type: "food", label: "π Village Plaza" },
|
| 520 |
+
"Club Eats": { pos: toCanvas(-1.8, -4.0), type: "food", label: "π¦ Club Eats" },
|
| 521 |
|
| 522 |
// Intermediate Junction Hubs
|
| 523 |
+
"Arena Crossroad": { pos: toCanvas(-3.8, -2.8), type: "hub", label: "π Arena Hub" },
|
| 524 |
+
"Becketts Interchange": { pos: toCanvas(0.2, 3.8), type: "hub", label: "π Becketts Hub" },
|
| 525 |
+
"South Bypass": { pos: toCanvas(-3.2, -3.8), type: "hub", label: "π South Hub" },
|
| 526 |
+
|
| 527 |
+
// Outer Gates
|
| 528 |
+
"Main Gate 1": { pos: toCanvas(-4.8, -2.8), type: "gate", label: "πͺ Main Gate 1" },
|
| 529 |
+
"South Gate 2": { pos: toCanvas(2.5, -4.5), type: "gate", label: "πͺ South Gate 2" },
|
| 530 |
+
"East Gate 3": { pos: toCanvas(5.2, 0.2), type: "gate", label: "πͺ East Gate 3" },
|
| 531 |
+
"West Gate 4": { pos: toCanvas(-4.5, 2.2), type: "gate", label: "πͺ West Gate 4" }
|
| 532 |
};
|
| 533 |
|
| 534 |
+
// Custom Waypoint path drawing function
|
| 535 |
+
function drawPath(points, color, isDashed = false) {
|
| 536 |
+
ctx.beginPath();
|
| 537 |
+
if (isDashed) ctx.setLineDash([8, 6]);
|
| 538 |
+
points.forEach((pt, idx) => {
|
| 539 |
+
const pos = typeof pt === 'string' ? nodes[pt].pos : toCanvas(pt.x, pt.y);
|
| 540 |
+
if (idx === 0) ctx.moveTo(pos.x, pos.y);
|
| 541 |
+
else ctx.lineTo(pos.x, pos.y);
|
| 542 |
+
});
|
| 543 |
+
ctx.strokeStyle = color;
|
| 544 |
+
ctx.lineWidth = isDashed ? 5 : 4;
|
| 545 |
+
ctx.stroke();
|
| 546 |
+
if (isDashed) ctx.setLineDash([]);
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
const pathColorG1 = isGate1Bottleneck ? "#ff4444" : "#00cc66";
|
| 550 |
const pathColorG3 = isGate3Bottleneck ? "#ff4444" : "#00cc66";
|
| 551 |
|
| 552 |
+
// Standard Pathways routed around the track perimeter
|
| 553 |
drawPath(["Hamilton GS", "Arena Crossroad"], pathColorG1);
|
| 554 |
drawPath(["Arena Crossroad", "Main Fan Zone"], pathColorG1);
|
| 555 |
drawPath(["Main Fan Zone", "Main Gate 1"], pathColorG1);
|
| 556 |
+
drawPath(["Village GS", {x: -2.8, y: 0.2}, "Village Plaza"], pathColorG1);
|
| 557 |
|
|
|
|
| 558 |
drawPath(["Becketts GS", "Becketts Interchange"], pathColorG3);
|
| 559 |
drawPath(["Becketts GS", "Becketts Food"], pathColorG3);
|
| 560 |
drawPath(["Becketts Food", "Stowe GS"], pathColorG3);
|
| 561 |
drawPath(["Stowe GS", "East Gate 3"], pathColorG3);
|
| 562 |
+
|
| 563 |
+
drawPath(["Becketts Interchange", {x: -2.5, y: 3.5}, "Village Plaza"], pathColorG1);
|
|
|
|
|
|
|
| 564 |
drawPath(["Village Plaza", "West Gate 4"], "#00cc66");
|
| 565 |
|
|
|
|
| 566 |
drawPath(["Hamilton GS", "South Bypass"], "#00cc66");
|
| 567 |
drawPath(["South Bypass", "Club Eats"], "#00cc66");
|
| 568 |
drawPath(["Club GS", "Club Eats"], "#00cc66");
|
| 569 |
drawPath(["Club Eats", "South Gate 2"], "#00cc66");
|
| 570 |
|
| 571 |
+
// Google Maps Detour Routing (Purple Lines) around perimeter
|
|
|
|
|
|
|
|
|
|
| 572 |
if (isGate1Bottleneck) {
|
|
|
|
| 573 |
drawPath(["Main Fan Zone", "Arena Crossroad", "South Bypass", "Club Eats", "South Gate 2"], "#a020f0", true);
|
| 574 |
+
drawPath(["Arena Crossroad", {x: -4.8, y: -0.2}, "West Gate 4"], "#a020f0", true);
|
|
|
|
|
|
|
| 575 |
}
|
| 576 |
|
| 577 |
if (isGate3Bottleneck) {
|
| 578 |
+
drawPath(["Becketts Interchange", {x: -2.5, y: 3.5}, "Village Plaza", "Main Fan Zone", "South Bypass", "South Gate 2"], "#a020f0", true);
|
|
|
|
| 579 |
}
|
| 580 |
|
| 581 |
+
// Draw Nodes, Badges & Labels
|
|
|
|
|
|
|
| 582 |
for (let key in nodes) {
|
| 583 |
const n = nodes[key];
|
| 584 |
ctx.font = "bold 11px Arial";
|
| 585 |
const textWidth = ctx.measureText(n.label).width;
|
| 586 |
const boxPadding = 7;
|
| 587 |
|
| 588 |
+
let boxColor = "#3498db";
|
| 589 |
+
if (n.type === "food") boxColor = "#f39c12";
|
| 590 |
+
if (n.type === "hub") boxColor = "#7f8c8d";
|
| 591 |
if (n.type === "gate") {
|
| 592 |
if ((key === "Main Gate 1" && isGate1Bottleneck) || (key === "East Gate 3" && isGate3Bottleneck)) {
|
| 593 |
boxColor = "#ff4444";
|
|
|
|
| 596 |
}
|
| 597 |
}
|
| 598 |
|
|
|
|
| 599 |
ctx.fillStyle = boxColor;
|
| 600 |
ctx.beginPath();
|
| 601 |
ctx.roundRect(
|
|
|
|
| 610 |
ctx.lineWidth = 1;
|
| 611 |
ctx.stroke();
|
| 612 |
|
|
|
|
| 613 |
ctx.fillStyle = "#ffffff";
|
| 614 |
ctx.textAlign = "center";
|
| 615 |
ctx.textBaseline = "middle";
|
| 616 |
ctx.fillText(n.label, n.pos.x, n.pos.y);
|
| 617 |
|
|
|
|
| 618 |
if (n.type === "food") {
|
| 619 |
const pendingCount = activeOrders.filter(o => o.location === key).length;
|
| 620 |
if (pendingCount > 0) {
|
|
|
|
| 633 |
}
|
| 634 |
}
|
| 635 |
|
| 636 |
+
// Legend
|
| 637 |
ctx.font = "11px Arial";
|
| 638 |
ctx.fillStyle = "#ffffff";
|
| 639 |
ctx.textAlign = "left";
|
|
|
|
| 676 |
}
|
| 677 |
}
|
| 678 |
|
|
|
|
| 679 |
window.onload = updateSimulation;
|
| 680 |
</script>
|
| 681 |
</body>
|