diff --git a/app/scout/routes.py b/app/scout/routes.py index b566f4a..cbd5dd8 100644 --- a/app/scout/routes.py +++ b/app/scout/routes.py @@ -35,10 +35,9 @@ def add(): return render_template("scouting/add.html") data = request.get_json() if request.is_json else request.form.to_dict() - # Convert the drawing coordinates from string to JSON if present - if "auto_path_coords" in data and isinstance(data["auto_path_coords"], str): + if "auto_path" in data and isinstance(data["auto_path"], str): try: - json.loads(data["auto_path_coords"]) # Validate JSON + data["auto_path"] = json.loads(data["auto_path"]) except json.JSONDecodeError: flash("Invalid path coordinates format", "error") return redirect(url_for("scouting.home")) diff --git a/app/static/images/blue-field-2025.png b/app/static/images/blue-field-2025.png deleted file mode 100644 index 892643c..0000000 Binary files a/app/static/images/blue-field-2025.png and /dev/null differ diff --git a/app/static/images/net-field-2025.png b/app/static/images/net-field-2025.png new file mode 100644 index 0000000..ca038a2 Binary files /dev/null and b/app/static/images/net-field-2025.png differ diff --git a/app/static/images/processor-field-2025.png b/app/static/images/processor-field-2025.png new file mode 100644 index 0000000..71c1ed3 Binary files /dev/null and b/app/static/images/processor-field-2025.png differ diff --git a/app/static/images/red-field-2025.png b/app/static/images/red-field-2025.png deleted file mode 100644 index 39be444..0000000 Binary files a/app/static/images/red-field-2025.png and /dev/null differ diff --git a/app/static/images/reef-field-2025.png b/app/static/images/reef-field-2025.png new file mode 100644 index 0000000..fd33bc6 Binary files /dev/null and b/app/static/images/reef-field-2025.png differ diff --git a/app/static/js/CanvasCoordinateSystem.js b/app/static/js/CanvasCoordinateSystem.js new file mode 100644 index 0000000..9e300a1 --- /dev/null +++ b/app/static/js/CanvasCoordinateSystem.js @@ -0,0 +1,141 @@ +class CanvasCoordinateSystem { + constructor(canvas) { + this.canvas = canvas; + this.container = document.getElementById('canvasContainer'); + this.ctx = canvas.getContext('2d'); + this.zoomLevel = 1.0; + this.standardZoom = 1.0; + this.minZoom = 0.5; + this.maxZoom = 3.0; + + // Initialize + this.resizeCanvas(); + window.addEventListener('resize', () => this.resizeCanvas()); + this.resetView(); + this.setupEventListeners(); + } + + resizeCanvas() { + const rect = this.container.getBoundingClientRect(); + this.canvas.width = rect.width; + this.canvas.height = rect.height; + } + + setupEventListeners() { + this.canvas.addEventListener('wheel', (e) => { + e.preventDefault(); + const rect = this.canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + const zoomFactor = e.deltaY < 0 ? 1.02 : 0.98; + this.zoom(mouseX, mouseY, zoomFactor); + }, { passive: false }); + + let initialDistance = 0; + let initialZoom = 1; + let isPinching = false; + + this.canvas.addEventListener('touchstart', (e) => { + if (e.touches.length === 2) { + e.preventDefault(); + isPinching = true; + initialDistance = Math.hypot( + e.touches[0].clientX - e.touches[1].clientX, + e.touches[0].clientY - e.touches[1].clientY + ); + initialZoom = this.zoomLevel; + } + }, { passive: false }); + + this.canvas.addEventListener('touchmove', (e) => { + if (e.touches.length === 2 && isPinching) { + e.preventDefault(); + const currentDistance = Math.hypot( + e.touches[0].clientX - e.touches[1].clientX, + e.touches[0].clientY - e.touches[1].clientY + ); + + const scale = currentDistance / initialDistance; + const dampedScale = scale > 1 ? + 1 + (scale - 1) * 0.1 : + 1 - (1 - scale) * 0.1; + + const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2; + const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2; + const rect = this.canvas.getBoundingClientRect(); + + this.zoom(midX - rect.left, midY - rect.top, dampedScale); + } + }, { passive: false }); + + this.canvas.addEventListener('touchend', () => { + isPinching = false; + }); + } + + resetView() { + this.zoomLevel = this.standardZoom; + this.updateTransform(); + // Update zoom level display on reset + const zoomLevelElement = document.getElementById('zoomLevel'); + if (zoomLevelElement) { + zoomLevelElement.textContent = `${Math.round(this.zoomLevel * 100)}%`; + } + } + + updateTransform() { + this.container.style.transformOrigin = `${this.originX}px ${this.originY}px`; + this.container.style.transform = `scale(${this.zoomLevel})`; + } + + clear() { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + } + + drawPath(points, color = 'red', width = 2) { + if (!points || points.length < 2) return; + + this.ctx.beginPath(); + this.ctx.moveTo(points[0].x, points[0].y); + + for (let i = 1; i < points.length; i++) { + this.ctx.lineTo(points[i].x, points[i].y); + } + + this.ctx.strokeStyle = color; + this.ctx.lineWidth = width; + this.ctx.stroke(); + } + + zoom(mouseX, mouseY, factor) { + const newZoom = this.zoomLevel * factor; + + if (newZoom >= this.minZoom && newZoom <= this.maxZoom) { + // Calculate the position relative to the container + const containerRect = this.container.getBoundingClientRect(); + const relativeX = mouseX / containerRect.width; + const relativeY = mouseY / containerRect.height; + + this.zoomLevel = newZoom; + + // Set transform origin as percentage values + this.container.style.transformOrigin = `${relativeX * 100}% ${relativeY * 100}%`; + this.container.style.transform = `scale(${this.zoomLevel})`; + + // Update zoom level display + const zoomLevelElement = document.getElementById('zoomLevel'); + if (zoomLevelElement) { + zoomLevelElement.textContent = `${Math.round(this.zoomLevel * 100)}%`; + } + } + } + + getDrawCoords(clientX, clientY) { + const rect = this.canvas.getBoundingClientRect(); + return { + x: (clientX - rect.left) / this.zoomLevel, + y: (clientY - rect.top) / this.zoomLevel + }; + } +} \ No newline at end of file diff --git a/app/static/js/scout.add.js b/app/static/js/scout.add.js index 9c30c7a..8df40d3 100644 --- a/app/static/js/scout.add.js +++ b/app/static/js/scout.add.js @@ -24,9 +24,9 @@ const updateTotal = () => { let climbPoints = 0; if (climbSuccess) { switch(climbType) { - case 'shallow': climbPoints = 3; break; - case 'deep': climbPoints = 5; break; - case 'park': climbPoints = 1; break; + case 'shallow': climbPoints = 6; break; + case 'deep': climbPoints = 12; break; + case 'park': climbPoints = 2; break; } } @@ -50,318 +50,241 @@ const updateMatchResult = () => { } }; -document.addEventListener('DOMContentLoaded', function() { - // Auto-capitalize event code - const eventCodeInput = document.querySelector('input[name="event_code"]'); - eventCodeInput.addEventListener('input', function(e) { - this.value = this.value.toUpperCase(); - }); - - // Calculate total points in real-time - const pointInputs = ['auto_points', 'teleop_points', 'endgame_points']; - const totalPointsDisplay = document.getElementById('totalPoints'); - - pointInputs.forEach(inputName => { - document.querySelector(`input[name="${inputName}"]`).addEventListener('input', updateTotal); - }); - - // Add event listeners for all scoring inputs - document.querySelectorAll('input[type="number"], input[type="checkbox"], select[name="climb_type"]') - .forEach(input => input.addEventListener('input', updateTotal)); - - // Initialize total - updateTotal(); - - // Auto-calculate match result - const allianceScoreInput = document.querySelector('input[name="alliance_score"]'); - const opponentScoreInput = document.querySelector('input[name="opponent_score"]'); - const matchResultInput = document.getElementById('match_result'); - - allianceScoreInput.addEventListener('input', updateMatchResult); - opponentScoreInput.addEventListener('input', updateMatchResult); - updateMatchResult(); // Initial calculation - - // Add form submission handler - const form = document.getElementById('scoutingForm'); - form.addEventListener('submit', async function(e) { - e.preventDefault(); - - const teamNumber = form.querySelector('input[name="team_number"]').value; - const eventCode = form.querySelector('input[name="event_code"]').value; - const matchNumber = form.querySelector('input[name="match_number"]').value; - - // Check if team already exists in this match - try { - const response = await fetch(`/scouting/check_team?team=${teamNumber}&event=${eventCode}&match=${matchNumber}`); - const data = await response.json(); - - if (data.exists) { - alert(`Team ${teamNumber} already exists in match ${matchNumber} for event ${eventCode}`); - return; - } - - // If team doesn't exist, submit the form - form.submit(); - } catch (error) { - console.error('Error checking team:', error); - // If check fails, allow form submission - form.submit(); - } - }); -}); - -const canvas = document.getElementById('autoPath'); -const ctx = canvas.getContext('2d'); +let canvas, coordSystem; let isDrawing = false; -let lastX = 0; -let lastY = 0; -let bgImage = new Image(); -let mobileRedImage = new Image(); -let mobileBlueImage = new Image(); -let pathHistory = []; let currentPath = []; -let imageScale = 1; -let imageOffset = { x: 0, y: 0 }; - -function resizeCanvas() { - const container = canvas.parentElement; - const containerWidth = container.clientWidth; - const containerHeight = container.clientHeight; - - // Set canvas size to match container - canvas.style.width = containerWidth + 'px'; - canvas.style.height = containerHeight + 'px'; - - // Set actual canvas dimensions - canvas.width = containerWidth * window.devicePixelRatio; - canvas.height = containerHeight * window.devicePixelRatio; - - // Scale context to match device pixel ratio - ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - - // Draw background and redraw paths - drawBackground(); - redrawPaths(); -} +let paths = []; -function loadImages() { - bgImage.src = "/static/images/field-2025.png"; - mobileRedImage.src = "/static/images/red-field-2025.png"; - mobileBlueImage.src = "/static/images/blue-field-2025.png"; -} - -function drawBackground() { - if (!bgImage.complete) { - return; +function initCanvas() { + canvas = document.getElementById('autoPath'); + if (!canvas) { + console.error('Canvas element not found'); + return; } + + coordSystem = new CanvasCoordinateSystem(canvas); - const canvasWidth = canvas.width / window.devicePixelRatio; - const canvasHeight = canvas.height / window.devicePixelRatio; - - ctx.clearRect(0, 0, canvas.width, canvas.height); - - const isMobile = window.innerWidth < 768; - const isRedAlliance = document.querySelector('input[name="alliance"][value="red"]').checked; + // Set canvas size based on container + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); + + // Mouse events + canvas.addEventListener('mousedown', startDrawing); + canvas.addEventListener('mousemove', draw); + canvas.addEventListener('mouseup', stopDrawing); + canvas.addEventListener('mouseleave', stopDrawing); - if (isMobile) { - // Use the pre-split field images for mobile - const mobileImage = isRedAlliance ? mobileRedImage : mobileBlueImage; - if (!mobileImage.complete) { - return; - } - - imageScale = Math.min( - canvasWidth / mobileImage.width, - canvasHeight / mobileImage.height - ); - - const scaledWidth = mobileImage.width * imageScale; - const scaledHeight = mobileImage.height * imageScale; - - imageOffset.x = (canvasWidth - scaledWidth) / 2; - imageOffset.y = (canvasHeight - scaledHeight) / 2; - - ctx.drawImage( - mobileImage, - imageOffset.x, imageOffset.y, scaledWidth, scaledHeight - ); - } else { - // Desktop view - show full field - imageScale = Math.min( - canvasWidth / bgImage.width, - canvasHeight / bgImage.height - ); - - const scaledWidth = bgImage.width * imageScale; - const scaledHeight = bgImage.height * imageScale; - - imageOffset.x = (canvasWidth - scaledWidth) / 2; - imageOffset.y = (canvasHeight - scaledHeight) / 2; - - ctx.drawImage( - bgImage, - imageOffset.x, imageOffset.y, scaledWidth, scaledHeight - ); - } + // Touch events + canvas.addEventListener('touchstart', handleTouchStart, { passive: false }); + canvas.addEventListener('touchmove', handleTouchMove, { passive: false }); + canvas.addEventListener('touchend', handleTouchEnd); + } -function getPointerPosition(e) { - const rect = canvas.getBoundingClientRect(); - // Get raw coordinates relative to canvas, accounting for device pixel ratio - return { - x: ((e.touches ? e.touches[0].clientX : e.clientX) - rect.left), - y: ((e.touches ? e.touches[0].clientY : e.clientY) - rect.top) - }; +function resizeCanvas() { + const container = canvas.parentElement; + canvas.width = container.clientWidth; + canvas.height = container.clientHeight; + coordSystem.updateTransform(); + redrawPaths(); } function startDrawing(e) { e.preventDefault(); isDrawing = true; - const pos = getPointerPosition(e); - lastX = pos.x; - lastY = pos.y; - currentPath = [{ - x: normalizeCoordinate(pos.x, true), - y: normalizeCoordinate(pos.y, false) - }]; + const point = getPointFromEvent(e); + currentPath = [point]; + redrawPaths(); } function draw(e) { + if (!isDrawing) return; e.preventDefault(); - if (!isDrawing) { - return; + const point = getPointFromEvent(e); + currentPath.push(point); + redrawPaths(); +} + +function stopDrawing(e) { + if (!isDrawing) return; + e.preventDefault(); + isDrawing = false; + if (currentPath.length > 1) { + paths.push(currentPath); + updateHiddenInput(); } - - const pos = getPointerPosition(e); - - // Draw on screen - ctx.beginPath(); - ctx.strokeStyle = '#FF0000'; - ctx.lineWidth = 3; - ctx.lineCap = 'round'; - ctx.moveTo(lastX, lastY); - ctx.lineTo(pos.x, pos.y); - ctx.stroke(); - - // Store normalized coordinates - currentPath.push({ - x: normalizeCoordinate(pos.x, true), - y: normalizeCoordinate(pos.y, false) - }); - - lastX = pos.x; - lastY = pos.y; - - updatePathData(); + currentPath = []; } -function normalizeCoordinate(coord, isX = true) { - // Convert screen coordinate to normalized coordinate (0-1 range) - if (isX) { - return (coord - imageOffset.x) / (bgImage.width * imageScale); +function handleTouchStart(e) { + if (e.touches.length === 1) { // Only draw with one finger + e.preventDefault(); + const touch = e.touches[0]; + startDrawing({ + clientX: touch.clientX, + clientY: touch.clientY, + preventDefault: () => {} + }); } - return (coord - imageOffset.y) / (bgImage.height * imageScale); } -function denormalizeCoordinate(coord, isX = true) { - // Convert normalized coordinate back to screen coordinate - if (isX) { - return (coord * bgImage.width * imageScale) + imageOffset.x; +function handleTouchMove(e) { + if (e.touches.length === 1) { // Only draw with one finger + e.preventDefault(); + const touch = e.touches[0]; + draw({ + clientX: touch.clientX, + clientY: touch.clientY, + preventDefault: () => {} + }); } - return (coord * bgImage.height * imageScale) + imageOffset.y; } -function redrawPaths() { - drawBackground(); - ctx.strokeStyle = '#FF0000'; - ctx.lineWidth = 3; - ctx.lineCap = 'round'; - - pathHistory.forEach(path => { - if (path.length > 1) { - ctx.beginPath(); - - // Convert first point - const startX = denormalizeCoordinate(path[0].x, true); - const startY = denormalizeCoordinate(path[0].y, false); - ctx.moveTo(startX, startY); - - // Convert and draw remaining points - for (let i = 1; i < path.length; i++) { - const x = denormalizeCoordinate(path[i].x, true); - const y = denormalizeCoordinate(path[i].y, false); - ctx.lineTo(x, y); - } - ctx.stroke(); - } - }); - - updatePathData(); +function handleTouchEnd(e) { + e.preventDefault(); + stopDrawing({ preventDefault: () => {} }); } -function updatePathData() { - // Store the normalized path data as JSON - const pathData = { - points: pathHistory.concat(currentPath.length > 0 ? [currentPath] : []), - alliance: document.querySelector('input[name="alliance"]:checked').value, - device_type: window.innerWidth < 768 ? 'mobile' : 'desktop' - }; - document.getElementById('autoPathData').value = JSON.stringify(pathData); +function getPointFromEvent(e) { + const rect = canvas.getBoundingClientRect(); + return coordSystem.getDrawCoords(e.clientX, e.clientY); } -function stopDrawing() { - if (isDrawing) { - isDrawing = false; - if (currentPath.length > 1) { - pathHistory.push(currentPath); - } - currentPath = []; +function redrawPaths() { + coordSystem.clear(); + paths.forEach(path => { + coordSystem.drawPath(path); + }); + if (currentPath.length > 0) { + coordSystem.drawPath(currentPath); } } +function updateHiddenInput() { + const input = document.getElementById('auto_path'); + // Store the paths array as a JSON string + input.value = JSON.stringify(paths); +} + function undoLastPath() { - if (pathHistory.length > 0) { - pathHistory.pop(); - redrawPaths(); - } + paths.pop(); + redrawPaths(); + updateHiddenInput(); } function clearCanvas() { - pathHistory = []; + paths = []; currentPath = []; - drawBackground(); - document.getElementById('autoPathData').value = ''; + redrawPaths(); + updateHiddenInput(); } -// Event listeners -canvas.addEventListener('mousedown', startDrawing); -canvas.addEventListener('mousemove', draw); -canvas.addEventListener('mouseup', stopDrawing); -canvas.addEventListener('mouseleave', stopDrawing); +function resetZoom() { + coordSystem.resetView(); + redrawPaths(); +} -canvas.addEventListener('touchstart', startDrawing); -canvas.addEventListener('touchmove', draw); -canvas.addEventListener('touchend', stopDrawing); -canvas.addEventListener('touchcancel', stopDrawing); +function zoomIn(event) { + if (!coordSystem) return; + const rect = canvas.getBoundingClientRect(); + let mouseX, mouseY; + + if (event.touches) { // Touch event + mouseX = event.touches[0].clientX - rect.left; + mouseY = event.touches[0].clientY - rect.top; + } else if (event.clientX !== undefined) { // Mouse event + mouseX = event.clientX - rect.left; + mouseY = event.clientY - rect.top; + } else { // Button click without position + mouseX = rect.width / 2; + mouseY = rect.height / 2; + } + + coordSystem.zoom(mouseX, mouseY, 1.1); // Reduced from 1.2 + redrawPaths(); +} -// Prevent scrolling while drawing -canvas.addEventListener('touchstart', e => e.preventDefault()); -canvas.addEventListener('touchmove', e => e.preventDefault()); +function zoomOut(event) { + if (!coordSystem) return; + const rect = canvas.getBoundingClientRect(); + let mouseX, mouseY; + + if (event.touches) { // Touch event + mouseX = event.touches[0].clientX - rect.left; + mouseY = event.touches[0].clientY - rect.top; + } else if (event.clientX !== undefined) { // Mouse event + mouseX = event.clientX - rect.left; + mouseY = event.clientY - rect.top; + } else { // Button click without position + mouseX = rect.width / 2; + mouseY = rect.height / 2; + } + + coordSystem.zoom(mouseX, mouseY, 0.9); // Increased from 0.8 + redrawPaths(); +} -// Initialize -window.addEventListener('load', () => { - loadImages(); - bgImage.onload = resizeCanvas; - mobileRedImage.onload = resizeCanvas; - mobileBlueImage.onload = resizeCanvas; -}); -window.addEventListener('resize', () => { - resizeCanvas(); -}); +// Form-related functionality +document.addEventListener('DOMContentLoaded', function() { + // Initialize canvas + initCanvas(); + + // Auto-capitalize event code + const eventCodeInput = document.querySelector('input[name="event_code"]'); + if (eventCodeInput) { + eventCodeInput.addEventListener('input', function(e) { + this.value = this.value.toUpperCase(); + }); + } + + // Calculate total points in real-time + const pointInputs = ['auto_points', 'teleop_points', 'endgame_points']; + const totalPointsDisplay = document.getElementById('totalPoints'); -// Add listener for alliance selection change -document.querySelectorAll('input[name="alliance"]').forEach(radio => { - radio.addEventListener('change', () => { - clearCanvas(); // Clear existing paths when alliance changes + pointInputs.forEach(inputName => { + document.querySelector(`input[name="${inputName}"]`).addEventListener('input', updateTotal); }); -}); \ No newline at end of file + + // Add event listeners for all scoring inputs + document.querySelectorAll('input[type="number"], input[type="checkbox"], select[name="climb_type"]') + .forEach(input => input.addEventListener('input', updateTotal)); + + // Initialize total + updateTotal(); + + // Auto-calculate match result + const allianceScoreInput = document.querySelector('input[name="alliance_score"]'); + const opponentScoreInput = document.querySelector('input[name="opponent_score"]'); + const matchResultInput = document.getElementById('match_result'); + + allianceScoreInput.addEventListener('input', updateMatchResult); + opponentScoreInput.addEventListener('input', updateMatchResult); + updateMatchResult(); // Initial calculation + + // Form submission handler + const form = document.getElementById('scoutingForm'); + if (form) { + form.addEventListener('submit', async function(e) { + e.preventDefault(); + + const teamNumber = form.querySelector('input[name="team_number"]').value; + const eventCode = form.querySelector('input[name="event_code"]').value; + const matchNumber = form.querySelector('input[name="match_number"]').value; + + try { + const response = await fetch(`/scouting/check_team?team=${teamNumber}&event=${eventCode}&match=${matchNumber}`); + const data = await response.json(); + + if (data.exists) { + alert(`Team ${teamNumber} already exists in match ${matchNumber} for event ${eventCode}`); + return; + } + + form.submit(); + } catch (error) { + console.error('Error checking team:', error); + form.submit(); + } + }); + } +}); diff --git a/app/static/js/scout.edit.js b/app/static/js/scout.edit.js index 216dc60..dd5575e 100644 --- a/app/static/js/scout.edit.js +++ b/app/static/js/scout.edit.js @@ -1,360 +1,282 @@ -document.addEventListener('DOMContentLoaded', function() { - // Auto-capitalize event code - const eventCodeInput = document.querySelector('input[name="event_code"]'); - eventCodeInput.addEventListener('input', function(e) { - this.value = this.value.toUpperCase(); - }); - - // Calculate total points in real-time - const updateTotal = () => { - const coralPoints = [1, 2, 3, 4].reduce((sum, level) => { - return sum + (parseInt(document.querySelector(`input[name="coral_level${level}"]`).value) || 0) * level; - }, 0); - - const algaeNet = (parseInt(document.querySelector('input[name="algae_net"]').value) || 0) * 2; - const algaeProcessor = (parseInt(document.querySelector('input[name="algae_processor"]').value) || 0) * 3; - const humanPlayerPoints = (parseInt(document.querySelector('input[name="human_player"]').value) || 0) * 2; - - const climbType = document.querySelector('select[name="climb_type"]').value; - const climbSuccess = document.querySelector('input[name="climb_success"]').checked; - let climbPoints = 0; - if (climbSuccess) { - switch(climbType) { - case 'shallow': climbPoints = 3; break; - case 'deep': climbPoints = 5; break; - case 'park': climbPoints = 1; break; - } +const updateTotal = () => { + // Auto Coral Points + const autoCoralPoints = [1, 2, 3, 4].reduce((sum, level) => { + return sum + (parseInt(document.querySelector(`input[name="auto_coral_level${level}"]`).value) || 0) * level; + }, 0); + + // Teleop Coral Points + const teleopCoralPoints = [1, 2, 3, 4].reduce((sum, level) => { + return sum + (parseInt(document.querySelector(`input[name="teleop_coral_level${level}"]`).value) || 0) * level; + }, 0); + + // Auto Algae Points + const autoAlgaeNet = (parseInt(document.querySelector('input[name="auto_algae_net"]').value) || 0) * 2; + const autoAlgaeProcessor = (parseInt(document.querySelector('input[name="auto_algae_processor"]').value) || 0) * 3; + + // Teleop Algae Points + const teleopAlgaeNet = (parseInt(document.querySelector('input[name="teleop_algae_net"]').value) || 0) * 2; + const teleopAlgaeProcessor = (parseInt(document.querySelector('input[name="teleop_algae_processor"]').value) || 0) * 3; + + const humanPlayerPoints = (parseInt(document.querySelector('input[name="human_player"]').value) || 0) * 2; + + const climbType = document.querySelector('select[name="climb_type"]').value; + const climbSuccess = document.querySelector('input[name="climb_success"]').checked; + let climbPoints = 0; + if (climbSuccess) { + switch(climbType) { + case 'shallow': climbPoints = 6; break; + case 'deep': climbPoints = 12; break; + case 'park': climbPoints = 2; break; } - - const total = coralPoints + algaeNet + algaeProcessor + humanPlayerPoints + climbPoints; - document.getElementById('totalPoints').textContent = total; - }; + } + + const total = autoCoralPoints + teleopCoralPoints + + autoAlgaeNet + autoAlgaeProcessor + + teleopAlgaeNet + teleopAlgaeProcessor + + humanPlayerPoints + climbPoints; + document.getElementById('totalPoints').textContent = total; +}; - // Add event listeners for all scoring inputs - document.querySelectorAll('input[type="number"], input[type="checkbox"], select[name="climb_type"]') - .forEach(input => input.addEventListener('input', updateTotal)); +let canvas, coordSystem; +let isDrawing = false; +let currentPath = []; +let paths = []; - // Initialize total - updateTotal(); +function initCanvas() { + canvas = document.getElementById('autoPath'); + if (!canvas) { + console.error('Canvas element not found'); + return; + } - // Add form submission handler with team check - const form = document.getElementById('scoutingForm'); - form.addEventListener('submit', async function(e) { - e.preventDefault(); - - const teamNumber = form.querySelector('input[name="team_number"]').value; - const eventCode = form.querySelector('input[name="event_code"]').value; - const matchNumber = form.querySelector('input[name="match_number"]').value; - const currentId = '{{ team_data._id }}'; + coordSystem = new CanvasCoordinateSystem(canvas); + + // Set canvas size based on container + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); + + // Mouse events + canvas.addEventListener('mousedown', startDrawing); + canvas.addEventListener('mousemove', draw); + canvas.addEventListener('mouseup', stopDrawing); + canvas.addEventListener('mouseleave', stopDrawing); + + // Touch events + canvas.addEventListener('touchstart', handleTouchStart, { passive: false }); + canvas.addEventListener('touchmove', handleTouchMove, { passive: false }); + canvas.addEventListener('touchend', handleTouchEnd); - // Check if team already exists in this match (excluding current entry) + // Load existing path data if available + const pathDataInput = document.getElementById('auto_path'); + if (pathDataInput && pathDataInput.value) { try { - const response = await fetch(`/scouting/check_team?team=${teamNumber}&event=${eventCode}&match=${matchNumber}¤t_id=${currentId}`); - const data = await response.json(); + const rawValue = pathDataInput.value; + const cleanValue = rawValue.replace(/^"(.*)"$/, '$1'); + const unescapedValue = cleanValue.replace(/\\"/g, '"'); + paths = JSON.parse(unescapedValue); - if (data.exists) { - alert(`Team ${teamNumber} already exists in match ${matchNumber} for event ${eventCode}`); - return; + if (!Array.isArray(paths)) { + console.error('Invalid path data format'); + paths = []; } - // If team doesn't exist or it's the same entry, submit the form - form.submit(); + redrawPaths(); } catch (error) { - console.error('Error checking team:', error); - // If check fails, allow form submission - form.submit(); + console.error('Error parsing path data:', error); + paths = []; } - }); -}); - -const canvas = document.getElementById('autoPath'); -const ctx = canvas.getContext('2d'); -let isDrawing = false; -let lastX = 0; -let lastY = 0; -let bgImage = new Image(); -let mobileRedImage = new Image(); -let mobileBlueImage = new Image(); -let pathHistory = []; -let currentPath = []; -let imageScale = 1; -let imageOffset = { x: 0, y: 0 }; - -function loadImages() { - bgImage.src = "/static/images/field-2025.png"; - mobileRedImage.src = "/static/images/red-field-2025.png"; - mobileBlueImage.src = "/static/images/blue-field-2025.png"; -} - -function resizeCanvas() { - const container = canvas.parentElement; - const containerWidth = container.clientWidth; - const containerHeight = container.clientHeight; - - // Set canvas size to match container - canvas.style.width = containerWidth + 'px'; - canvas.style.height = containerHeight + 'px'; - - // Set actual canvas dimensions - canvas.width = containerWidth * window.devicePixelRatio; - canvas.height = containerHeight * window.devicePixelRatio; - - // Scale context to match device pixel ratio - ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - - // Draw background and redraw paths - drawBackground(); - redrawPaths(); + } } -function drawBackground() { - if (!bgImage.complete) { - return; - } - - const canvasWidth = canvas.width / window.devicePixelRatio; - const canvasHeight = canvas.height / window.devicePixelRatio; - - ctx.clearRect(0, 0, canvas.width, canvas.height); - - const isMobile = window.innerWidth < 768; - const isRedAlliance = document.querySelector('input[name="alliance"][value="red"]').checked; - - if (isMobile) { - // Use the pre-split field images for mobile - const mobileImage = isRedAlliance ? mobileRedImage : mobileBlueImage; - if (!mobileImage.complete) { - return; - } - - imageScale = Math.min( - canvasWidth / mobileImage.width, - canvasHeight / mobileImage.height - ); - - const scaledWidth = mobileImage.width * imageScale; - const scaledHeight = mobileImage.height * imageScale; - - imageOffset.x = (canvasWidth - scaledWidth) / 2; - imageOffset.y = (canvasHeight - scaledHeight) / 2; - - ctx.drawImage( - mobileImage, - imageOffset.x, imageOffset.y, scaledWidth, scaledHeight - ); - } else { - // Desktop view - show full field - imageScale = Math.min( - canvasWidth / bgImage.width, - canvasHeight / bgImage.height - ); - - const scaledWidth = bgImage.width * imageScale; - const scaledHeight = bgImage.height * imageScale; - - imageOffset.x = (canvasWidth - scaledWidth) / 2; - imageOffset.y = (canvasHeight - scaledHeight) / 2; - - ctx.drawImage( - bgImage, - imageOffset.x, imageOffset.y, scaledWidth, scaledHeight - ); +function handleTouchStart(e) { + if (e.touches.length === 1) { + e.preventDefault(); + const touch = e.touches[0]; + startDrawing({ + clientX: touch.clientX, + clientY: touch.clientY, + preventDefault: () => {} + }); } } -function getPointerPosition(e) { - const rect = canvas.getBoundingClientRect(); - // Get raw coordinates relative to canvas, accounting for device pixel ratio - return { - x: ((e.touches ? e.touches[0].clientX : e.clientX) - rect.left), - y: ((e.touches ? e.touches[0].clientY : e.clientY) - rect.top) - }; +function handleTouchMove(e) { + if (e.touches.length === 1) { + e.preventDefault(); + const touch = e.touches[0]; + draw({ + clientX: touch.clientX, + clientY: touch.clientY, + preventDefault: () => {} + }); + } } -function normalizeCoordinate(coord, isX = true) { - // Convert screen coordinate to normalized coordinate (0-1 range) - const rect = canvas.getBoundingClientRect(); - if (isX) { - return (coord - imageOffset.x) / (bgImage.width * imageScale); - } - return (coord - imageOffset.y) / (bgImage.height * imageScale); +function handleTouchEnd(e) { + e.preventDefault(); + stopDrawing({ preventDefault: () => {} }); } -function denormalizeCoordinate(coord, isX = true) { - // Convert normalized coordinate back to screen coordinate - if (isX) { - return (coord * bgImage.width * imageScale) + imageOffset.x; - } - return (coord * bgImage.height * imageScale) + imageOffset.y; +function resizeCanvas() { + const container = canvas.parentElement; + canvas.width = container.clientWidth; + canvas.height = container.clientHeight; + coordSystem.updateTransform(); + redrawPaths(); } function startDrawing(e) { e.preventDefault(); isDrawing = true; - const pos = getPointerPosition(e); - lastX = pos.x; - lastY = pos.y; - currentPath = [{ - x: normalizeCoordinate(pos.x, true), - y: normalizeCoordinate(pos.y, false) - }]; + const point = getPointFromEvent(e); + currentPath = [point]; + redrawPaths(); } function draw(e) { + if (!isDrawing) return; e.preventDefault(); - if (!isDrawing) { - return; - } - - const pos = getPointerPosition(e); - - // Draw on screen - ctx.beginPath(); - ctx.strokeStyle = '#FF0000'; - ctx.lineWidth = 3; - ctx.lineCap = 'round'; - ctx.moveTo(lastX, lastY); - ctx.lineTo(pos.x, pos.y); - ctx.stroke(); - - // Store normalized coordinates - currentPath.push({ - x: normalizeCoordinate(pos.x, true), - y: normalizeCoordinate(pos.y, false) - }); - - lastX = pos.x; - lastY = pos.y; - - // Store the path data - updatePathData(); + const point = getPointFromEvent(e); + currentPath.push(point); + redrawPaths(); } -function stopDrawing() { - if (isDrawing) { - isDrawing = false; - if (currentPath.length > 1) { - pathHistory.push(currentPath); - } - currentPath = []; +function stopDrawing(e) { + if (!isDrawing) return; + e.preventDefault(); + isDrawing = false; + if (currentPath.length > 1) { + paths.push(currentPath); + updateHiddenInput(); } + currentPath = []; } -function undoLastPath() { - if (pathHistory.length > 0) { - pathHistory.pop(); - redrawPaths(); - } +function getPointFromEvent(e) { + const rect = canvas.getBoundingClientRect(); + return coordSystem.getDrawCoords(e.clientX, e.clientY); } function redrawPaths() { - drawBackground(); - ctx.strokeStyle = '#FF0000'; - ctx.lineWidth = 3; - ctx.lineCap = 'round'; - - // Draw all paths using denormalized coordinates - pathHistory.forEach(path => { - if (path.length > 1) { - ctx.beginPath(); - - // Convert first point - const startX = denormalizeCoordinate(path[0].x, true); - const startY = denormalizeCoordinate(path[0].y, false); - ctx.moveTo(startX, startY); - - // Convert and draw remaining points - for (let i = 1; i < path.length; i++) { - const x = denormalizeCoordinate(path[i].x, true); - const y = denormalizeCoordinate(path[i].y, false); - ctx.lineTo(x, y); - } - ctx.stroke(); - } + coordSystem.clear(); + paths.forEach(path => { + coordSystem.drawPath(path); }); - - updatePathData(); + if (currentPath.length > 0) { + coordSystem.drawPath(currentPath); + } +} + +function updateHiddenInput() { + const input = document.getElementById('auto_path'); + input.value = JSON.stringify(paths); +} + +function undoLastPath() { + paths.pop(); + redrawPaths(); + updateHiddenInput(); } function clearCanvas() { - pathHistory = []; + paths = []; currentPath = []; - // Reset to original field background - bgImage.src = "/static/images/field-2025.png"; - document.getElementById('autoPathData').value = ''; + redrawPaths(); + updateHiddenInput(); } -function loadExistingPath(pathData) { - if (!pathData || pathData === "None") { - console.log('No valid path data to load'); - return; - } +function resetZoom() { + coordSystem.resetView(); + redrawPaths(); +} + +function zoomIn(event) { + if (!coordSystem) return; + const rect = canvas.getBoundingClientRect(); + let mouseX, mouseY; - try { - const data = JSON.parse(pathData); - pathHistory = data.points || []; - - // Set alliance if it exists in the data - if (data.alliance) { - const allianceInput = document.querySelector(`input[name="alliance"][value="${data.alliance}"]`); - if (allianceInput) { - allianceInput.checked = true; - } - } - - redrawPaths(); - } catch (error) { - console.error('Failed to load path data:', error); + if (event.touches) { // Touch event + mouseX = event.touches[0].clientX - rect.left; + mouseY = event.touches[0].clientY - rect.top; + } else if (event.clientX !== undefined) { // Mouse event + mouseX = event.clientX - rect.left; + mouseY = event.clientY - rect.top; + } else { // Button click without position + mouseX = rect.width / 2; + mouseY = rect.height / 2; } + + coordSystem.zoom(mouseX, mouseY, 1.1); + redrawPaths(); } -function updatePathData() { - // Store the normalized path data as JSON - const pathData = { - points: pathHistory.concat(currentPath.length > 0 ? [currentPath] : []), - alliance: document.querySelector('input[name="alliance"]:checked').value - }; - document.getElementById('autoPathData').value = JSON.stringify(pathData); +function zoomOut(event) { + if (!coordSystem) return; + const rect = canvas.getBoundingClientRect(); + let mouseX, mouseY; + + if (event.touches) { // Touch event + mouseX = event.touches[0].clientX - rect.left; + mouseY = event.touches[0].clientY - rect.top; + } else if (event.clientX !== undefined) { // Mouse event + mouseX = event.clientX - rect.left; + mouseY = event.clientY - rect.top; + } else { // Button click without position + mouseX = rect.width / 2; + mouseY = rect.height / 2; + } + + coordSystem.zoom(mouseX, mouseY, 0.9); + redrawPaths(); } -// Event listeners -canvas.addEventListener('mousedown', startDrawing); -canvas.addEventListener('mousemove', draw); -canvas.addEventListener('mouseup', stopDrawing); -canvas.addEventListener('mouseleave', stopDrawing); +// Single DOMContentLoaded event handler +document.addEventListener('DOMContentLoaded', function() { + // Initialize canvas first + initCanvas(); -canvas.addEventListener('touchstart', startDrawing); -canvas.addEventListener('touchmove', draw); -canvas.addEventListener('touchend', stopDrawing); -canvas.addEventListener('touchcancel', stopDrawing); + // Auto-capitalize event code + const eventCodeInput = document.querySelector('input[name="event_code"]'); + if (eventCodeInput) { + eventCodeInput.addEventListener('input', function(e) { + this.value = this.value.toUpperCase(); + }); + } -// Prevent scrolling while drawing -canvas.addEventListener('touchstart', e => e.preventDefault()); -canvas.addEventListener('touchmove', e => e.preventDefault()); + // Add event listeners for all scoring inputs + document.querySelectorAll('input[type="number"], input[type="checkbox"], select[name="climb_type"]') + .forEach(input => input.addEventListener('input', updateTotal)); -// Initialize -window.addEventListener('load', () => { - loadImages(); - - // Load existing path if available - const existingPathData = document.getElementById('autoPathData').value; - if (existingPathData && existingPathData !== "None") { - bgImage.onload = () => { - resizeCanvas(); - loadExistingPath(existingPathData); - }; + // Initialize total + updateTotal(); + + // Form submission handler + const form = document.getElementById('scoutingForm'); + if (form) { + form.addEventListener('submit', async function(e) { + e.preventDefault(); + + const teamNumber = form.querySelector('input[name="team_number"]').value; + const eventCode = form.querySelector('input[name="event_code"]').value; + const matchNumber = form.querySelector('input[name="match_number"]').value; + const currentId = form.querySelector('input[name="current_id"]')?.value; + + try { + const response = await fetch(`/scouting/check_team?team=${teamNumber}&event=${eventCode}&match=${matchNumber}¤t_id=${currentId}`); + const data = await response.json(); + + if (data.exists) { + alert(`Team ${teamNumber} already exists in match ${matchNumber} for event ${eventCode}`); + return; + } + + // Update the auto_path input before submitting + updateHiddenInput(); + form.submit(); + } catch (error) { + console.error('Error checking team:', error); + form.submit(); + } + }); } - - mobileRedImage.onload = resizeCanvas; - mobileBlueImage.onload = resizeCanvas; }); -window.addEventListener('resize', () => { - resizeCanvas(); -}); - -// Add listener for alliance selection change -document.querySelectorAll('input[name="alliance"]').forEach(radio => { - radio.addEventListener('change', () => { - clearCanvas(); // Clear existing paths when alliance changes - }); -}); \ No newline at end of file diff --git a/app/static/js/scout.list.js b/app/static/js/scout.list.js index b4b7bfa..bbb9b17 100644 --- a/app/static/js/scout.list.js +++ b/app/static/js/scout.list.js @@ -1,15 +1,109 @@ -// Global variables -let canvas, ctx, bgImage, mobileRedImage, mobileBlueImage, imageScale; -let filterType, searchInput, eventSections; +let modalCanvas, modalCoordSystem; +let currentPathData = null; -// Initialize images right away -bgImage = new Image(); -mobileRedImage = new Image(); -mobileBlueImage = new Image(); +function showAutoPath(pathData, autoNotes, deviceType) { + currentPathData = pathData; + + // Show the modal + const modal = document.getElementById('autoPathModal'); + modal.classList.remove('hidden'); + + // Initialize canvas and coordinate system if not already done + if (!modalCanvas) { + modalCanvas = document.getElementById('modalAutoPath'); + modalCoordSystem = new CanvasCoordinateSystem(modalCanvas); + + // Set canvas size to match container + resizeModalCanvas(); + window.addEventListener('resize', resizeModalCanvas); + } + + redrawPaths(); + + // Set auto notes + const notesElement = document.getElementById('modalAutoNotes'); + if (notesElement) { + notesElement.textContent = autoNotes || 'No notes available'; + } +} + +function resizeModalCanvas() { + const container = modalCanvas.parentElement; + modalCanvas.width = container.clientWidth; + modalCanvas.height = container.clientHeight; + modalCoordSystem.updateTransform(); + redrawPaths(); +} + +function redrawPaths() { + if (!modalCoordSystem || !currentPathData) return; + + modalCoordSystem.clear(); + + let paths = currentPathData; + if (typeof currentPathData === 'string') { + try { + paths = JSON.parse(currentPathData); + } catch (e) { + console.error('Error parsing path data:', e); + return; + } + } + + if (Array.isArray(paths)) { + paths.forEach(path => { + if (Array.isArray(path) && path.length > 0) { + const formattedPath = path.map(point => { + if (typeof point === 'object' && 'x' in point && 'y' in point) { + return { + x: (point.x / 1000) * modalCanvas.width, + y: (point.y / 300) * modalCanvas.height + }; + } + return null; + }).filter(point => point !== null); + + if (formattedPath.length > 0) { + modalCoordSystem.drawPath(formattedPath, '#3b82f6', 3); + } + } + }); + } +} + +function zoomIn(event) { + if (!modalCoordSystem) return; + const rect = modalCanvas.getBoundingClientRect(); + let mouseX = rect.width / 2; + let mouseY = rect.height / 2; + + modalCoordSystem.zoom(mouseX, mouseY, 1.1); + redrawPaths(); +} -bgImage.src = "/static/images/field-2025.png"; -mobileRedImage.src = "/static/images/red-field-2025.png"; -mobileBlueImage.src = "/static/images/blue-field-2025.png"; +function zoomOut(event) { + if (!modalCoordSystem) return; + const rect = modalCanvas.getBoundingClientRect(); + let mouseX = rect.width / 2; + let mouseY = rect.height / 2; + + modalCoordSystem.zoom(mouseX, mouseY, 0.9); + redrawPaths(); +} + +function resetZoom() { + if (!modalCoordSystem) return; + modalCoordSystem.resetView(); + redrawPaths(); +} + +function closeAutoPathModal() { + const modal = document.getElementById('autoPathModal'); + modal.classList.add('hidden'); + if (modalCoordSystem) { + modalCoordSystem.resetView(); + } +} const filterRows = () => { const searchTerm = searchInput.value.toLowerCase(); @@ -53,148 +147,15 @@ document.addEventListener('DOMContentLoaded', function() { filterType.addEventListener('change', filterRows); } - // Initialize canvas when page loads - initializeCanvas(); - - // Add window resize listener - window.addEventListener('resize', resizeCanvas); -}); - -function initializeCanvas() { - canvas = document.getElementById('modalAutoPath'); - if (!canvas) { - return; - } - - ctx = canvas.getContext('2d'); - resizeCanvas(); -} - -function resizeCanvas() { - if (!canvas || !ctx) { - return; - } - - const container = canvas.parentElement; - const containerWidth = container.clientWidth; - const containerHeight = container.clientHeight; - - canvas.style.width = containerWidth + 'px'; - canvas.style.height = containerHeight + 'px'; - - canvas.width = containerWidth * window.devicePixelRatio; - canvas.height = containerHeight * window.devicePixelRatio; - - ctx.scale(window.devicePixelRatio, window.devicePixelRatio); -} - -function drawPath(pathData, deviceType) { - if (!canvas || !ctx) { - return; - } - - const currentIsMobile = window.innerWidth < 768; - const alliance = pathData.alliance || 'red'; - const points = pathData.points || []; - - // Choose the appropriate background image - const image = currentIsMobile ? - (alliance === 'red' ? mobileRedImage : mobileBlueImage) : - bgImage; - - // Wait for image to load - if (!image.complete) { - image.onload = () => drawPath(pathData, deviceType); - return; - } - - // Calculate scaling factors and offsets - const canvasWidth = canvas.width / window.devicePixelRatio; - const canvasHeight = canvas.height / window.devicePixelRatio; - - imageScale = Math.min( - canvasWidth / image.width, - canvasHeight / image.height - ); - - const scaledWidth = image.width * imageScale; - const scaledHeight = image.height * imageScale; - - const offsetX = (canvasWidth - scaledWidth) / 2; - const offsetY = (canvasHeight - scaledHeight) / 2; - - // Clear and draw background - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - ctx.drawImage(image, offsetX, offsetY, scaledWidth, scaledHeight); - - // Draw paths - ctx.strokeStyle = '#FF0000'; - ctx.lineWidth = 3; - ctx.lineCap = 'round'; - - // Draw all paths using denormalized coordinates - points.forEach(path => { - if (path && path.length > 1) { - ctx.beginPath(); - - // Convert first point - const startX = (path[0].x * image.width * imageScale) + offsetX; - const startY = (path[0].y * image.height * imageScale) + offsetY; - ctx.moveTo(startX, startY); - - // Convert and draw remaining points - for (let i = 1; i < path.length; i++) { - const x = (path[i].x * image.width * imageScale) + offsetX; - const y = (path[i].y * image.height * imageScale) + offsetY; - ctx.lineTo(x, y); - } - ctx.stroke(); - } - }); -} - -function showAutoPath(pathData, autoNotes, deviceType) { - // Ensure pathData is an object - let pathObj = pathData; - if (typeof pathData === 'string') { - try { - pathObj = JSON.parse(pathData); - } catch (error) { - console.error('Failed to parse path data:', error); - return; - } - } - - // Show the modal const modal = document.getElementById('autoPathModal'); - modal.classList.remove('hidden'); - - // Initialize canvas if not already done - if (!canvas) { - initializeCanvas(); - } - - // Update auto notes - const notesElement = document.getElementById('modalAutoNotes'); - notesElement.textContent = autoNotes || 'No notes available'; - - // Resize canvas and draw path - resizeCanvas(); - - // Ensure pathData has the expected structure - if (!pathObj || !pathObj.points) { - console.error('Invalid path data structure'); - return; + if (modal) { + modal.addEventListener('click', function(e) { + if (e.target === modal) { + closeAutoPathModal(); + } + }); } - - // Draw the path - drawPath(pathObj, deviceType); -} - -function closeAutoPathModal() { - const modal = document.getElementById('autoPathModal'); - modal.classList.add('hidden'); -} +}); function updateTotal() { // Auto scoring @@ -213,8 +174,17 @@ function updateTotal() { const teleopAlgaeNet = (parseInt(document.querySelector('input[name="teleop_algae_net"]').value) || 0) * 2; const teleopAlgaeProcessor = (parseInt(document.querySelector('input[name="teleop_algae_processor"]').value) || 0) * 3; const humanPlayerPoints = (parseInt(document.querySelector('input[name="human_player"]').value) || 0) * 2; + const climbType = document.querySelector('select[name="climb_type"]').value; + const climbSuccess = document.querySelector('input[name="climb_success"]').checked; + let climbPoints = 0; + if (climbSuccess) { + switch(climbType) { + case 'shallow': climbPoints = 6; break; + case 'deep': climbPoints = 12; break; + case 'park': climbPoints = 2; break; + } + } - // ... existing climb points calculation ... const total = autoCoralPoints + autoAlgaeNet + autoAlgaeProcessor + teleopCoralPoints + teleopAlgaeNet + teleopAlgaeProcessor + diff --git a/app/templates/scouting/add.html b/app/templates/scouting/add.html index 0b4e190..a305292 100644 --- a/app/templates/scouting/add.html +++ b/app/templates/scouting/add.html @@ -274,12 +274,50 @@
Draw the robot's autonomous path
Draw the robot's autonomous path