/** * Isometric 3D Warehouse Visualization * Professional order picking visualization with animated trolleys */ const ISO = { // Isometric projection angles ANGLE: Math.PI / 6, // 30 degrees // Tile dimensions TILE_WIDTH: 48, TILE_HEIGHT: 24, // Shelf dimensions (in tiles) SHELF_WIDTH: 3, SHELF_DEPTH: 1, SHELF_HEIGHT: 2, // Warehouse layout: 5 columns (A-E) x 3 rows COLS: 5, ROWS: 3, // Spacing between shelves AISLE_WIDTH: 2, // Colors COLORS: { floor: '#e2e8f0', floorGrid: '#cbd5e1', shelfTop: '#ffffff', shelfFront: '#f1f5f9', shelfSide: '#e2e8f0', shelfBorder: '#94a3b8', shadow: 'rgba(0, 0, 0, 0.1)', trolley: [ '#ef4444', // red '#3b82f6', // blue '#10b981', // green '#f59e0b', // amber '#8b5cf6', // purple '#06b6d4', // cyan '#ec4899', // pink '#84cc16', // lime ], path: 'rgba(59, 130, 246, 0.3)', pathActive: 'rgba(59, 130, 246, 0.6)', }, // Animation state animationId: null, isSolving: false, trolleyAnimations: new Map(), currentSolution: null, // Canvas state canvas: null, ctx: null, dpr: 1, width: 0, height: 0, originX: 0, originY: 0, // Grid offset to center the warehouse gridOffsetX: 0, gridOffsetY: 0, }; // Column/Row mapping const COLUMNS = ['A', 'B', 'C', 'D', 'E']; const ROWS = ['1', '2', '3']; /** * Convert isometric coordinates to screen coordinates */ function isoToScreen(x, y, z = 0) { // Apply grid offset to center the warehouse const centeredX = x - ISO.gridOffsetX; const centeredY = y - ISO.gridOffsetY; const screenX = ISO.originX + (centeredX - centeredY) * (ISO.TILE_WIDTH / 2); const screenY = ISO.originY + (centeredX + centeredY) * (ISO.TILE_HEIGHT / 2) - z * ISO.TILE_HEIGHT; return { x: screenX, y: screenY }; } /** * Get trolley color by ID */ function getTrolleyColor(trolleyId) { const index = (parseInt(trolleyId) - 1) % ISO.COLORS.trolley.length; return ISO.COLORS.trolley[index]; } /** * Initialize the canvas */ function initWarehouseCanvas() { const container = document.getElementById('warehouseContainer'); if (!container) return; let canvas = document.getElementById('warehouseCanvas'); if (!canvas) { canvas = document.createElement('canvas'); canvas.id = 'warehouseCanvas'; container.appendChild(canvas); } ISO.canvas = canvas; ISO.ctx = canvas.getContext('2d'); ISO.dpr = window.devicePixelRatio || 1; // Calculate canvas size based on warehouse dimensions const totalWidth = (ISO.COLS * (ISO.SHELF_WIDTH + ISO.AISLE_WIDTH) + ISO.AISLE_WIDTH) * ISO.TILE_WIDTH; const totalHeight = (ISO.ROWS * (ISO.SHELF_DEPTH + ISO.AISLE_WIDTH) + ISO.AISLE_WIDTH) * ISO.TILE_WIDTH; // Isometric dimensions ISO.width = totalWidth + 200; ISO.height = totalHeight / 2 + 300; // Set canvas size with HiDPI support canvas.width = ISO.width * ISO.dpr; canvas.height = ISO.height * ISO.dpr; canvas.style.width = ISO.width + 'px'; canvas.style.height = ISO.height + 'px'; ISO.ctx.scale(ISO.dpr, ISO.dpr); // Calculate grid center offset for centering const gridSize = ISO.COLS * (ISO.SHELF_WIDTH + ISO.AISLE_WIDTH) + ISO.AISLE_WIDTH; const gridDepth = ISO.ROWS * (ISO.SHELF_DEPTH + ISO.AISLE_WIDTH) + ISO.AISLE_WIDTH; ISO.gridOffsetX = gridSize / 2; ISO.gridOffsetY = gridDepth / 2; // Set origin point (center of canvas) ISO.originX = ISO.width / 2; ISO.originY = ISO.height / 2 - 50; // Slightly above center } /** * Draw the warehouse floor grid */ function drawFloor() { const ctx = ISO.ctx; const gridSize = ISO.COLS * (ISO.SHELF_WIDTH + ISO.AISLE_WIDTH) + ISO.AISLE_WIDTH; const gridDepth = ISO.ROWS * (ISO.SHELF_DEPTH + ISO.AISLE_WIDTH) + ISO.AISLE_WIDTH; // Draw floor tiles for (let x = 0; x < gridSize; x++) { for (let y = 0; y < gridDepth; y++) { const p1 = isoToScreen(x, y); const p2 = isoToScreen(x + 1, y); const p3 = isoToScreen(x + 1, y + 1); const p4 = isoToScreen(x, y + 1); ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.lineTo(p3.x, p3.y); ctx.lineTo(p4.x, p4.y); ctx.closePath(); ctx.fillStyle = ISO.COLORS.floor; ctx.fill(); ctx.strokeStyle = ISO.COLORS.floorGrid; ctx.lineWidth = 0.5; ctx.stroke(); } } } /** * Draw a 3D shelf at grid position */ function drawShelf(col, row, label) { const ctx = ISO.ctx; // Calculate grid position const gridX = ISO.AISLE_WIDTH + col * (ISO.SHELF_WIDTH + ISO.AISLE_WIDTH); const gridY = ISO.AISLE_WIDTH + row * (ISO.SHELF_DEPTH + ISO.AISLE_WIDTH); const w = ISO.SHELF_WIDTH; const d = ISO.SHELF_DEPTH; const h = ISO.SHELF_HEIGHT; // Get corner points const topFront = [ isoToScreen(gridX, gridY + d, h), isoToScreen(gridX + w, gridY + d, h), isoToScreen(gridX + w, gridY, h), isoToScreen(gridX, gridY, h), ]; const bottomFront = [ isoToScreen(gridX, gridY + d, 0), isoToScreen(gridX + w, gridY + d, 0), ]; const bottomSide = [ isoToScreen(gridX + w, gridY, 0), ]; // Draw shadow ctx.beginPath(); const shadowOffset = 0.3; const s1 = isoToScreen(gridX + shadowOffset, gridY + d + shadowOffset, 0); const s2 = isoToScreen(gridX + w + shadowOffset, gridY + d + shadowOffset, 0); const s3 = isoToScreen(gridX + w + shadowOffset, gridY + shadowOffset, 0); const s4 = isoToScreen(gridX + shadowOffset, gridY + shadowOffset, 0); ctx.moveTo(s1.x, s1.y); ctx.lineTo(s2.x, s2.y); ctx.lineTo(s3.x, s3.y); ctx.lineTo(s4.x, s4.y); ctx.closePath(); ctx.fillStyle = ISO.COLORS.shadow; ctx.fill(); // Draw front face ctx.beginPath(); ctx.moveTo(topFront[0].x, topFront[0].y); ctx.lineTo(topFront[1].x, topFront[1].y); ctx.lineTo(bottomFront[1].x, bottomFront[1].y); ctx.lineTo(bottomFront[0].x, bottomFront[0].y); ctx.closePath(); ctx.fillStyle = ISO.COLORS.shelfFront; ctx.fill(); ctx.strokeStyle = ISO.COLORS.shelfBorder; ctx.lineWidth = 1; ctx.stroke(); // Draw side face ctx.beginPath(); ctx.moveTo(topFront[1].x, topFront[1].y); ctx.lineTo(topFront[2].x, topFront[2].y); ctx.lineTo(bottomSide[0].x, bottomSide[0].y); ctx.lineTo(bottomFront[1].x, bottomFront[1].y); ctx.closePath(); ctx.fillStyle = ISO.COLORS.shelfSide; ctx.fill(); ctx.strokeStyle = ISO.COLORS.shelfBorder; ctx.stroke(); // Draw top face ctx.beginPath(); ctx.moveTo(topFront[0].x, topFront[0].y); ctx.lineTo(topFront[1].x, topFront[1].y); ctx.lineTo(topFront[2].x, topFront[2].y); ctx.lineTo(topFront[3].x, topFront[3].y); ctx.closePath(); ctx.fillStyle = ISO.COLORS.shelfTop; ctx.fill(); ctx.strokeStyle = ISO.COLORS.shelfBorder; ctx.stroke(); // Draw label const centerX = (topFront[0].x + topFront[1].x + topFront[2].x + topFront[3].x) / 4; const centerY = (topFront[0].y + topFront[1].y + topFront[2].y + topFront[3].y) / 4; ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#475569'; ctx.fillText(label, centerX, centerY); } /** * Draw all shelves */ function drawShelves() { for (let col = 0; col < ISO.COLS; col++) { for (let row = 0; row < ISO.ROWS; row++) { const label = COLUMNS[col] + ',' + ROWS[row]; drawShelf(col, row, label); } } } /** * Draw a trolley at position */ function drawTrolley(x, y, color, trolleyId, progress = 1) { const ctx = ISO.ctx; const pos = isoToScreen(x, y, 0.3); // Trolley body dimensions const bodyWidth = 20; const bodyHeight = 14; const bodyDepth = 8; // Draw trolley body (isometric box) ctx.save(); ctx.translate(pos.x, pos.y); // Shadow ctx.beginPath(); ctx.ellipse(0, 6, 12, 6, 0, 0, Math.PI * 2); ctx.fillStyle = 'rgba(0, 0, 0, 0.15)'; ctx.fill(); // Body - front ctx.beginPath(); ctx.moveTo(-bodyWidth/2, -bodyDepth); ctx.lineTo(bodyWidth/2, -bodyDepth); ctx.lineTo(bodyWidth/2, bodyHeight - bodyDepth); ctx.lineTo(-bodyWidth/2, bodyHeight - bodyDepth); ctx.closePath(); ctx.fillStyle = color; ctx.fill(); ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)'; ctx.lineWidth = 1; ctx.stroke(); // Body - top ctx.beginPath(); ctx.moveTo(-bodyWidth/2, -bodyDepth); ctx.lineTo(0, -bodyDepth - 6); ctx.lineTo(bodyWidth/2, -bodyDepth); ctx.lineTo(0, -bodyDepth + 3); ctx.closePath(); const lighterColor = lightenColor(color, 20); ctx.fillStyle = lighterColor; ctx.fill(); ctx.stroke(); // Handle ctx.beginPath(); ctx.moveTo(-bodyWidth/2 + 3, -bodyDepth - 2); ctx.lineTo(-bodyWidth/2 + 3, -bodyDepth - 12); ctx.lineTo(bodyWidth/2 - 3, -bodyDepth - 12); ctx.lineTo(bodyWidth/2 - 3, -bodyDepth - 2); ctx.strokeStyle = darkenColor(color, 20); ctx.lineWidth = 2; ctx.stroke(); // Trolley number badge ctx.beginPath(); ctx.arc(0, -bodyDepth - 16, 10, 0, Math.PI * 2); ctx.fillStyle = 'white'; ctx.fill(); ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke(); ctx.font = 'bold 11px -apple-system, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = color; ctx.fillText(trolleyId, 0, -bodyDepth - 16); ctx.restore(); } /** * Lighten a hex color */ function lightenColor(hex, percent) { const num = parseInt(hex.slice(1), 16); const amt = Math.round(2.55 * percent); const R = Math.min(255, (num >> 16) + amt); const G = Math.min(255, ((num >> 8) & 0x00FF) + amt); const B = Math.min(255, (num & 0x0000FF) + amt); return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1); } /** * Darken a hex color */ function darkenColor(hex, percent) { const num = parseInt(hex.slice(1), 16); const amt = Math.round(2.55 * percent); const R = Math.max(0, (num >> 16) - amt); const G = Math.max(0, ((num >> 8) & 0x00FF) - amt); const B = Math.max(0, (num & 0x0000FF) - amt); return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1); } /** * Convert warehouse location to grid position * Returns position in aisle next to the shelf */ function locationToGrid(location) { if (!location) return { x: 1, y: 1 }; // Parse shelving ID like "(A, 1)" const shelvingId = location.shelvingId || ''; const match = shelvingId.match(/\(([A-E]),\s*(\d)\)/); let col = 0, row = 0; if (match) { col = COLUMNS.indexOf(match[1]); row = parseInt(match[2]) - 1; } // Calculate shelf's grid position const shelfGridX = ISO.AISLE_WIDTH + col * (ISO.SHELF_WIDTH + ISO.AISLE_WIDTH); const shelfGridY = ISO.AISLE_WIDTH + row * (ISO.SHELF_DEPTH + ISO.AISLE_WIDTH); // Position in the aisle in front of the shelf (below it in grid terms) const aisleY = shelfGridY + ISO.SHELF_DEPTH + 0.5; // Adjust X for side (LEFT/RIGHT of shelf) const side = location.side; let gridX; if (side === 'LEFT') { gridX = shelfGridX - 0.5; // Left aisle } else { gridX = shelfGridX + ISO.SHELF_WIDTH + 0.5; // Right aisle } return { x: gridX, y: aisleY }; } /** * Get the main aisle Y position (horizontal corridor) */ function getMainAisleY() { // Main aisle runs at the bottom of the warehouse return (ISO.ROWS * (ISO.SHELF_DEPTH + ISO.AISLE_WIDTH)) + ISO.AISLE_WIDTH + 0.5; } /** * Get vertical aisle X positions (between shelves) */ function getVerticalAisleX(col) { // Aisle to the left of column 'col' return ISO.AISLE_WIDTH + col * (ISO.SHELF_WIDTH + ISO.AISLE_WIDTH) - 0.5; } /** * Build a path that follows aisles from start to end * Uses a simple strategy: go to main aisle, traverse horizontally, go up to destination */ function buildAislePath(start, end) { const path = [start]; // If start and end are very close, just go directly const dx = Math.abs(start.x - end.x); const dy = Math.abs(start.y - end.y); if (dx < 2 && dy < 2) { path.push(end); return path; } const mainAisleY = getMainAisleY(); // Strategy: go down to main aisle (or near it), traverse, then go up // First, go to the nearest vertical aisle const startAisleY = Math.max(start.y, mainAisleY - 1); const endAisleY = Math.max(end.y, mainAisleY - 1); // Move to horizontal travel position if (Math.abs(start.y - startAisleY) > 0.5) { path.push({ x: start.x, y: startAisleY }); } // Move horizontally to align with destination column if (Math.abs(start.x - end.x) > 0.5) { path.push({ x: end.x, y: startAisleY }); } // Move vertically to destination if (Math.abs(path[path.length - 1].y - end.y) > 0.5) { path.push({ x: end.x, y: end.y }); } // Add final destination if different from last point const last = path[path.length - 1]; if (Math.abs(last.x - end.x) > 0.1 || Math.abs(last.y - end.y) > 0.1) { path.push(end); } return path; } /** * Build trolley path from steps with proper aisle routing * Returns { path: [], pickupIndices: [] } */ function buildTrolleyPath(trolley, steps) { const waypoints = []; const waypointTypes = []; // 'start', 'pickup', 'end' // Start position if (trolley.location) { waypoints.push(locationToGrid(trolley.location)); waypointTypes.push('start'); } // Add each step location for (const step of steps) { if (step.orderItem && step.orderItem.product && step.orderItem.product.location) { waypoints.push(locationToGrid(step.orderItem.product.location)); waypointTypes.push('pickup'); } } // Return to start if (waypoints.length > 1 && trolley.location) { waypoints.push(locationToGrid(trolley.location)); waypointTypes.push('end'); } if (waypoints.length <= 1) { return { path: waypoints, pickupIndices: [] }; } // Build full path with aisle routing between each waypoint const fullPath = [waypoints[0]]; const pickupIndices = []; for (let i = 1; i < waypoints.length; i++) { const segmentPath = buildAislePath(waypoints[i - 1], waypoints[i]); // Skip first point (it's the same as last point of previous segment) for (let j = 1; j < segmentPath.length; j++) { fullPath.push(segmentPath[j]); } // Track pickup point index if (waypointTypes[i] === 'pickup') { pickupIndices.push(fullPath.length - 1); } } return { path: fullPath, pickupIndices: pickupIndices }; } /** * Draw trolley path */ function drawPath(path, color, pickupIndices, active = false) { if (path.length < 2) return; const ctx = ISO.ctx; ctx.beginPath(); const start = isoToScreen(path[0].x, path[0].y, 0.1); ctx.moveTo(start.x, start.y); for (let i = 1; i < path.length; i++) { const point = isoToScreen(path[i].x, path[i].y, 0.1); ctx.lineTo(point.x, point.y); } ctx.strokeStyle = active ? ISO.COLORS.pathActive : ISO.COLORS.path; ctx.lineWidth = active ? 4 : 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.stroke(); // Draw pickup markers only at actual pickup points let pickupNum = 1; for (const idx of pickupIndices) { if (idx >= 0 && idx < path.length) { const point = isoToScreen(path[idx].x, path[idx].y, 0.5); ctx.beginPath(); ctx.arc(point.x, point.y, 8, 0, Math.PI * 2); ctx.fillStyle = color; ctx.fill(); ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.stroke(); ctx.font = 'bold 9px -apple-system, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'white'; ctx.fillText(pickupNum.toString(), point.x, point.y); pickupNum++; } } } /** * Get position along path at progress (0-1) */ function getPositionOnPath(path, progress) { if (!path || path.length === 0) return { x: 0, y: 0 }; if (path.length === 1) return path[0]; const totalSegments = path.length - 1; const segmentProgress = progress * totalSegments; const currentSegment = Math.min(Math.floor(segmentProgress), totalSegments - 1); const segmentT = segmentProgress - currentSegment; const start = path[currentSegment]; const end = path[currentSegment + 1]; // Safety check for undefined points if (!start || !end) return path[0] || { x: 0, y: 0 }; return { x: start.x + (end.x - start.x) * segmentT, y: start.y + (end.y - start.y) * segmentT, }; } /** * Render the full warehouse scene */ function renderWarehouse(solution) { if (!ISO.ctx) return; const ctx = ISO.ctx; ctx.clearRect(0, 0, ISO.width, ISO.height); // Draw floor drawFloor(); // Draw shelves (back to front for proper overlap) drawShelves(); if (!solution || !solution.trolleys) return; // Build step lookup const stepLookup = new Map(); for (const step of solution.trolleySteps || []) { stepLookup.set(step.id, step); } // Draw paths and trolleys const trolleys = solution.trolleys || []; for (const trolley of trolleys) { const steps = (trolley.steps || []).map(ref => typeof ref === 'string' ? stepLookup.get(ref) : ref ).filter(s => s); const color = getTrolleyColor(trolley.id); const pathData = buildTrolleyPath(trolley, steps); const path = pathData.path; const pickupIndices = pathData.pickupIndices; // Draw path if (path.length > 1) { drawPath(path, color, pickupIndices, ISO.isSolving); } // Draw trolley const anim = ISO.trolleyAnimations.get(trolley.id); let pos; if (anim && ISO.isSolving && path.length > 1) { const now = Date.now(); const elapsed = now - anim.startTime; const progress = (elapsed % anim.duration) / anim.duration; pos = getPositionOnPath(path, progress); } else if (path.length > 0) { pos = path[0]; } else { pos = locationToGrid(trolley.location); } if (pos) { drawTrolley(pos.x, pos.y, color, trolley.id); } } } /** * Animation loop - only runs when solving to animate trolley positions */ function animate() { if (!ISO.isSolving) { ISO.animationId = null; return; } // Only re-render if we have a valid solution // The animation loop provides smooth trolley movement if (ISO.currentSolution && ISO.currentSolution.trolleys) { renderWarehouse(ISO.currentSolution); } ISO.animationId = requestAnimationFrame(animate); } /** * Start solving animation */ function startWarehouseAnimation(solution) { ISO.isSolving = true; ISO.currentSolution = solution; ISO.trolleyAnimations.clear(); // Initialize animations for each trolley const stepLookup = new Map(); for (const step of solution.trolleySteps || []) { stepLookup.set(step.id, step); } for (const trolley of solution.trolleys || []) { // Get step IDs for signature const stepIds = (trolley.steps || []).map(ref => typeof ref === 'string' ? ref : ref.id ); const steps = stepIds.map(id => stepLookup.get(id)).filter(s => s); const pathData = buildTrolleyPath(trolley, steps); const path = pathData.path; const duration = Math.max(3000, path.length * 400); ISO.trolleyAnimations.set(trolley.id, { startTime: Date.now() + parseInt(trolley.id) * 200, duration: duration, path: path, stepSignature: stepIds.join(','), // Track initial signature }); } if (!ISO.animationId) { animate(); } } /** * Update animation with new solution data */ function updateWarehouseAnimation(solution) { console.log('[updateWarehouseAnimation] Called with', solution?.trolleys?.length, 'trolleys'); ISO.currentSolution = solution; // Build step lookup for resolving references const stepLookup = new Map(); for (const step of solution.trolleySteps || []) { stepLookup.set(step.id, step); } let anyPathChanged = false; for (const trolley of solution.trolleys || []) { // Get step IDs for this trolley (for change detection) const stepIds = (trolley.steps || []).map(ref => typeof ref === 'string' ? ref : ref.id ); // Resolve to full step objects const steps = stepIds.map(id => stepLookup.get(id)).filter(s => s); const pathData = buildTrolleyPath(trolley, steps); const path = pathData.path; const existingAnim = ISO.trolleyAnimations.get(trolley.id); if (existingAnim) { // Create signature of step assignments to detect any change const oldSignature = existingAnim.stepSignature || ''; const newSignature = stepIds.join(','); const pathChanged = oldSignature !== newSignature; if (pathChanged) { anyPathChanged = true; console.log(`[PATH CHANGE] Trolley ${trolley.id}:`, { old: oldSignature.substring(0, 50), new: newSignature.substring(0, 50), oldLen: oldSignature.split(',').filter(x=>x).length, newLen: stepIds.length }); } existingAnim.path = path; existingAnim.stepSignature = newSignature; existingAnim.duration = Math.max(3000, path.length * 400); // Reset animation timing when path changes to prevent position jumps if (pathChanged) { existingAnim.startTime = Date.now(); } } else { ISO.trolleyAnimations.set(trolley.id, { startTime: Date.now(), duration: Math.max(3000, path.length * 400), path: path, stepSignature: stepIds.join(','), }); anyPathChanged = true; } } // Visual feedback when paths change if (anyPathChanged) { console.log('[RENDER] Paths changed, forcing immediate render'); // Flash the canvas border to indicate update if (ISO.canvas) { ISO.canvas.style.outline = '3px solid #10b981'; setTimeout(() => { ISO.canvas.style.outline = 'none'; }, 200); } } // Force immediate render with updated paths renderWarehouse(solution); // Restart animation loop if it died if (ISO.isSolving && !ISO.animationId) { animate(); } } /** * Stop animation */ function stopWarehouseAnimation() { ISO.isSolving = false; if (ISO.animationId) { cancelAnimationFrame(ISO.animationId); ISO.animationId = null; } // Render final state if (ISO.currentSolution) { renderWarehouse(ISO.currentSolution); } } /** * Update legend with trolley info */ function updateLegend(solution, distances) { const container = document.getElementById('trolleyLegend'); if (!container) return; container.innerHTML = ''; const stepLookup = new Map(); for (const step of solution.trolleySteps || []) { stepLookup.set(step.id, step); } for (const trolley of solution.trolleys || []) { const steps = (trolley.steps || []).map(ref => typeof ref === 'string' ? stepLookup.get(ref) : ref ).filter(s => s); const color = getTrolleyColor(trolley.id); const distance = distances ? distances.get(trolley.id) || 0 : 0; const item = document.createElement('div'); item.className = 'legend-item'; item.innerHTML = `
Trolley ${trolley.id} ${steps.length} items `; container.appendChild(item); } } // Export for app.js window.initWarehouseCanvas = initWarehouseCanvas; window.renderWarehouse = renderWarehouse; window.startWarehouseAnimation = startWarehouseAnimation; window.updateWarehouseAnimation = updateWarehouseAnimation; window.stopWarehouseAnimation = stopWarehouseAnimation; window.updateLegend = updateLegend; window.getTrolleyColor = getTrolleyColor;