From 48e60ec0b95f8240c0c1830aad57fbbf65bc1f8b Mon Sep 17 00:00:00 2001 From: Jerry Date: Mon, 6 Jan 2025 22:32:08 -0500 Subject: [PATCH] Add files todo and update **Done:** Models.py, list.html (?), add.html, field-2025.png **In Progress**: Edit.html **Not started**: Leaderboard.html, matches.html, team.html --- app/models.py | 54 +++- app/scout/routes.py | 223 +++++++++++----- app/scout/scouting_utils.py | 207 +++++++++++---- app/static/images/field-2025.png | Bin 0 -> 18593 bytes app/templates/scouting/add.html | 340 ++++++++++++++++++++---- app/templates/scouting/edit.html | 303 +++++++++++++-------- app/templates/scouting/leaderboard.html | 43 ++- app/templates/scouting/list.html | 140 +++++++--- app/templates/scouting/matches.html | 41 ++- app/templates/scouting/team.html | 45 ++-- 10 files changed, 1009 insertions(+), 387 deletions(-) create mode 100644 app/static/images/field-2025.png diff --git a/app/models.py b/app/models.py index 648a304..660a887 100644 --- a/app/models.py +++ b/app/models.py @@ -71,19 +71,39 @@ def __init__(self, data): self.team_number = data.get('team_number') self.match_number = data.get('match_number') self.event_code = data.get('event_code') - self.auto_points = data.get('auto_points', 0) - self.teleop_points = data.get('teleop_points', 0) - self.endgame_points = data.get('endgame_points', 0) - self.total_points = data.get('total_points', 0) - self.notes = data.get('notes', '') self.alliance = data.get('alliance', '') - self.match_result = data.get('match_result', '') + + # Coral scoring + self.coral_level1 = data.get('coral_level1', 0) + self.coral_level2 = data.get('coral_level2', 0) + self.coral_level3 = data.get('coral_level3', 0) + self.coral_level4 = data.get('coral_level4', 0) + + # Algae scoring + self.algae_net = data.get('algae_net', 0) + self.algae_processor = data.get('algae_processor', 0) + self.human_player = data.get('human_player', 0) # Number of successful shots + + # Climb + self.climb_type = data.get('climb_type', '') # 'shallow', 'deep', 'park', or '' + self.climb_success = data.get('climb_success', False) + + # Defense + self.defense_rating = data.get('defense_rating', 1) # 1-5 scale + self.defense_notes = data.get('defense_notes', '') + + # Auto + self.auto_path = data.get('auto_path', '') # Store canvas data as base64 + self.auto_notes = data.get('auto_notes', '') + + # Notes + self.notes = data.get('notes', '') # Scouter information self.scouter_id = data.get('scouter_id') self.scouter_name = data.get('scouter_name') self.scouter_team = data.get('scouter_team') - self.is_owner = data.get('is_owner', True) # Default to False if not set + self.is_owner = data.get('is_owner', True) @classmethod def create_from_db(cls, data): @@ -95,13 +115,21 @@ def to_dict(self): 'team_number': self.team_number, 'match_number': self.match_number, 'event_code': self.event_code, - 'auto_points': self.auto_points, - 'teleop_points': self.teleop_points, - 'endgame_points': self.endgame_points, - 'total_points': self.total_points, - 'notes': self.notes, 'alliance': self.alliance, - 'match_result': self.match_result, + 'coral_level1': self.coral_level1, + 'coral_level2': self.coral_level2, + 'coral_level3': self.coral_level3, + 'coral_level4': self.coral_level4, + 'algae_net': self.algae_net, + 'algae_processor': self.algae_processor, + 'human_player': self.human_player, + 'climb_type': self.climb_type, + 'climb_success': self.climb_success, + 'defense_rating': self.defense_rating, + 'defense_notes': self.defense_notes, + 'auto_path': self.auto_path, + 'auto_notes': self.auto_notes, + 'notes': self.notes, 'scouter_id': self.scouter_id, 'scouter_name': self.scouter_name, 'scouter_team': self.scouter_team, diff --git a/app/scout/routes.py b/app/scout/routes.py index 3868b38..18b0259 100644 --- a/app/scout/routes.py +++ b/app/scout/routes.py @@ -31,7 +31,8 @@ def wrapper(*args, **kwargs): @login_required def add_scouting_data(): if request.method == "POST": - data = request.get_json() if request.is_json else request.form + data = request.get_json() if request.is_json else request.form.to_dict() + success, message = scouting_manager.add_scouting_data( data, current_user.get_id() ) @@ -175,9 +176,21 @@ async def compare_teams(): { "event_code": entry["event_code"], "match_number": entry["match_number"], - "auto_points": entry["auto_points"], - "teleop_points": entry["teleop_points"], - "endgame_points": entry["endgame_points"], + "coral_levels": [ + entry["coral_level1"], + entry["coral_level2"], + entry["coral_level3"], + entry["coral_level4"] + ], + "algae": { + "net": entry["algae_net"], + "processor": entry["algae_processor"], + "human_player": entry["human_player"] + }, + "climb": { + "type": entry["climb_type"], + "success": entry["climb_success"] + }, "total_points": entry["total_points"], "notes": entry["notes"], "scouter": entry["scouter"]["username"], @@ -192,7 +205,14 @@ async def compare_teams(): "city": team.get("city"), "state_prov": team.get("state_prov"), "country": team.get("country"), - "stats": stats, + "stats": { + "matches_played": stats["matches_played"], + "avg_coral": stats["avg_coral"], + "avg_algae": stats["avg_algae"], + "climb_success_rate": stats["climb_success_rate"], + "defense_rating": stats["avg_defense"], + "total_points": stats["total_points"] + }, "scouting_data": scouting_entries, } @@ -236,13 +256,94 @@ async def search_teams(): "from": "users", "localField": "scouter_id", "foreignField": "_id", - "as": "scouter", + "as": "scouter" } }, {"$unwind": "$scouter"}, + { + "$project": { + "_id": 1, + "team_number": 1, + "match_number": 1, + "event_code": 1, + "coral_level1": 1, + "coral_level2": 1, + "coral_level3": 1, + "coral_level4": 1, + "algae_net": 1, + "algae_processor": 1, + "human_player": 1, + "climb_type": 1, + "climb_success": 1, + "defense_rating": 1, + "defense_notes": 1, + "auto_path": 1, + "auto_notes": 1, + "total_points": 1, + "notes": 1, + "alliance": 1, + "scouter_id": 1, + "scouter_name": "$scouter.username", + "scouter_team": "$scouter.teamNumber" + } + } ] team_scouting_data = list(scouting_manager.db.team_data.aggregate(pipeline)) + + # Calculate statistics + matches_played = len(team_scouting_data) + if matches_played > 0: + coral_totals = [sum([ + entry["coral_level1"], + entry["coral_level2"], + entry["coral_level3"], + entry["coral_level4"] + ]) for entry in team_scouting_data] + + algae_totals = [ + entry["algae_net"] + entry["algae_processor"] + for entry in team_scouting_data + ] + + successful_climbs = sum(bool(entry["climb_success"]) + for entry in team_scouting_data) + + stats = { + "matches_played": matches_played, + "coral_stats": { + "level1": sum(entry["coral_level1"] for entry in team_scouting_data) / matches_played, + "level2": sum(entry["coral_level2"] for entry in team_scouting_data) / matches_played, + "level3": sum(entry["coral_level3"] for entry in team_scouting_data) / matches_played, + "level4": sum(entry["coral_level4"] for entry in team_scouting_data) / matches_played, + }, + "algae_stats": { + "net": sum(entry["algae_net"] for entry in team_scouting_data) / matches_played, + "processor": sum(entry["algae_processor"] for entry in team_scouting_data) / matches_played, + "human_player_rate": sum(bool(entry["human_player"]) + for entry in team_scouting_data) / matches_played + }, + "climb_success_rate": sum(bool(entry["climb_success"]) + for entry in team_scouting_data) / matches_played, + "avg_defense": sum(entry["defense_rating"] for entry in team_scouting_data) / matches_played + } + else: + stats = { + "matches_played": 0, + "coral_stats": { + "level1": 0, + "level2": 0, + "level3": 0, + "level4": 0 + }, + "algae_stats": { + "net": 0, + "processor": 0, + "human_player_rate": 0 + }, + "climb_success_rate": 0, + "avg_defense": 0 + } scouting_entries = [ { @@ -391,9 +492,19 @@ def matches(): "number": "$team_number", "total_points": "$total_points", "alliance": "$alliance", - "auto_points": "$auto_points", - "teleop_points": "$teleop_points", - "endgame_points": "$endgame_points" + "coral_total": { + "$sum": [ + "$coral_level1", + "$coral_level2", + "$coral_level3", + "$coral_level4" + ] + }, + "algae_total": { + "$sum": ["$algae_net", "$algae_processor"] + }, + "climb_type": "$climb_type", + "climb_success": "$climb_success" } } } @@ -414,7 +525,11 @@ def matches(): "red_teams": red_teams, "blue_teams": blue_teams, "red_score": sum(t["total_points"] for t in red_teams), - "blue_score": sum(t["total_points"] for t in blue_teams) + "blue_score": sum(t["total_points"] for t in blue_teams), + "red_coral_total": sum(t["coral_total"] for t in red_teams), + "red_algae_total": sum(t["algae_total"] for t in red_teams), + "blue_coral_total": sum(t["coral_total"] for t in blue_teams), + "blue_algae_total": sum(t["algae_total"] for t in blue_teams) }) return render_template("scouting/matches.html", matches=matches) @@ -422,75 +537,39 @@ def matches(): flash(f"Error fetching matches: {str(e)}", "error") return render_template("scouting/matches.html", matches=[]) -@scouting_bp.route("/scouting/team/") +@scouting_bp.route("/team/") @login_required -def team_view(team_number): +def view_team(team_number): try: - # Get team stats from MongoDB - pipeline = [ - {"$match": {"team_number": team_number}}, - { - "$group": { - "_id": "$team_number", - "matches_played": {"$sum": 1}, - "total_points": {"$sum": "$total_points"}, - "auto_points": {"$avg": "$auto_points"}, - "teleop_points": {"$avg": "$teleop_points"}, - "endgame_points": {"$avg": "$endgame_points"}, - "matches": { - "$push": { - "match_number": "$match_number", - "event_code": "$event_code", - "auto_points": "$auto_points", - "teleop_points": "$teleop_points", - "endgame_points": "$endgame_points", - "total_points": "$total_points", - "notes": {"$ifNull": ["$notes", ""]} - } - } - } - } - ] - - team_data = list(scouting_manager.db.team_data.aggregate(pipeline)) - - if not team_data: - flash("No scouting data found for this team", "error") - return redirect(url_for("scouting.list_scouting_data")) - - team_stats = team_data[0] - - # Convert Decimal128 to float for JSON serialization - stats = { - "matches_played": team_stats["matches_played"], - "total_points": float(team_stats["total_points"]), - "auto_points": float(team_stats["auto_points"]), - "teleop_points": float(team_stats["teleop_points"]), - "endgame_points": float(team_stats["endgame_points"]) - } - - matches = [ - { - "event_code": str(match["event_code"]), - "match_number": int(match["match_number"]), - "auto_points": float(match["auto_points"]), - "teleop_points": float(match["teleop_points"]), - "endgame_points": float(match["endgame_points"]), - "total_points": float(match["total_points"]), - "notes": str(match["notes"]), - } - for match in team_stats["matches"] - ] + matches = scouting_manager.get_team_matches(team_number) + stats = scouting_manager.get_team_stats(team_number) + + # Calculate averages and success rates + if stats["matches_played"] > 0: + stats["avg_coral"] = ( + stats["total_coral"] / stats["matches_played"] + ) + stats["avg_algae"] = ( + stats["total_algae"] / stats["matches_played"] + ) + stats["climb_success_rate"] = ( + stats["successful_climbs"] / stats["matches_played"] + ) + else: + stats.update({ + "avg_coral": 0, + "avg_algae": 0, + "climb_success_rate": 0 + }) + return render_template( "scouting/team.html", team_number=team_number, - stats=stats, - matches=matches + matches=matches, + stats=stats ) - except Exception as e: - print(f"Error loading team data: {str(e)}") - flash(f"Error loading team data: {str(e)}", "error") + flash(f"Error fetching team data: {str(e)}", "error") return redirect(url_for("scouting.list_scouting_data")) @scouting_bp.route("/scouting/check_team") diff --git a/app/scout/scouting_utils.py b/app/scout/scouting_utils.py index dd92516..2d9e18d 100644 --- a/app/scout/scouting_utils.py +++ b/app/scout/scouting_utils.py @@ -105,55 +105,49 @@ def add_scouting_data(self, data, scouter_id): if (alliance == "red" and len(red_teams) >= 3) or (alliance == "blue" and len(blue_teams) >= 3): return False, f"Cannot add more teams to {alliance} alliance (maximum 3)" - # Calculate alliance scores - red_score = sum(t["total_points"] for t in red_teams) - blue_score = sum(t["total_points"] for t in blue_teams) - - # Add current team's points to their alliance - current_points = ( - int(data["auto_points"]) - + int(data["teleop_points"]) - + int(data["endgame_points"]) - ) - if alliance == "red": - red_score += current_points - alliance_score = red_score - opponent_score = blue_score - else: - blue_score += current_points - alliance_score = blue_score - opponent_score = red_score - - # Determine match result - if alliance_score > opponent_score: - match_result = "won" - elif alliance_score < opponent_score: - match_result = "lost" - else: - match_result = "tie" - + # Process form data team_data = { "team_number": team_number, "event_code": data["event_code"], "match_number": int(data["match_number"]), - "auto_points": int(data["auto_points"]), - "teleop_points": int(data["teleop_points"]), - "endgame_points": int(data["endgame_points"]), - "total_points": current_points, - "notes": data["notes"], - "scouter_id": ObjectId(scouter_id), "alliance": alliance, - "alliance_score": alliance_score, - "opponent_score": opponent_score, - "match_result": match_result, + + # Coral scoring + "coral_level1": int(data.get("coral_level1", 0)), + "coral_level2": int(data.get("coral_level2", 0)), + "coral_level3": int(data.get("coral_level3", 0)), + "coral_level4": int(data.get("coral_level4", 0)), + + # Algae scoring + "algae_net": int(data.get("algae_net", 0)), + "algae_processor": int(data.get("algae_processor", 0)), + "human_player": int(data.get("human_player", 0)), + + # Climb + "climb_type": data.get("climb_type", ""), + "climb_success": bool(data.get("climb_success", False)), + + # Defense + "defense_rating": int(data.get("defense_rating", 1)), + "defense_notes": data.get("defense_notes", ""), + + # Auto + "auto_path": data.get("auto_path", ""), + "auto_notes": data.get("auto_notes", ""), + + # Notes + "notes": data.get("notes", ""), + + # Metadata + "scouter_id": ObjectId(scouter_id), "created_at": datetime.now(timezone.utc), } - self.db.team_data.insert_one(team_data) - logger.info(f"Added new scouting data for team {data['team_number']}") - return True, "Data added successfully" + result = self.db.team_data.insert_one(team_data) + return True, str(result.inserted_id) + except Exception as e: - logger.error(f"Error adding scouting data: {str(e)}") + logger.error(f"Error adding team data: {str(e)}") return False, str(e) @with_mongodb_retry(retries=3, delay=2) @@ -176,13 +170,21 @@ def get_all_scouting_data(self): "team_number": 1, "match_number": 1, "event_code": 1, - "auto_points": 1, - "teleop_points": 1, - "endgame_points": 1, - "total_points": 1, + "coral_level1": 1, + "coral_level2": 1, + "coral_level3": 1, + "coral_level4": 1, + "algae_net": 1, + "algae_processor": 1, + "human_player": 1, + "climb_type": 1, + "climb_success": 1, + "defense_rating": 1, + "defense_notes": 1, + "auto_path": 1, + "auto_notes": 1, "notes": 1, "alliance": 1, - "match_result": 1, "scouter_id": 1, "scouter_name": "$scouter.username", "scouter_team": "$scouter.teamNumber" @@ -191,9 +193,9 @@ def get_all_scouting_data(self): ] team_data = list(self.db.team_data.aggregate(pipeline)) - return [TeamData.create_from_db(data) for data in team_data] + return team_data except Exception as e: - print(f"Error fetching team data: {e}") + logger.error(f"Error fetching team data: {str(e)}") return [] @with_mongodb_retry(retries=3, delay=2) @@ -237,17 +239,33 @@ def update_team_data(self, team_id, data, scouter_id): "team_number": int(data["team_number"]), "event_code": data["event_code"], "match_number": int(data["match_number"]), - "auto_points": int(data["auto_points"]), - "teleop_points": int(data["teleop_points"]), - "endgame_points": int(data["endgame_points"]), - "total_points": ( - int(data["auto_points"]) - + int(data["teleop_points"]) - + int(data["endgame_points"]) - ), - "notes": data["notes"], "alliance": data.get("alliance", "red"), - "match_result": data.get("match_result", ""), + + # Coral scoring + "coral_level1": int(data.get("coral_level1", 0)), + "coral_level2": int(data.get("coral_level2", 0)), + "coral_level3": int(data.get("coral_level3", 0)), + "coral_level4": int(data.get("coral_level4", 0)), + + # Algae scoring + "algae_net": int(data.get("algae_net", 0)), + "algae_processor": int(data.get("algae_processor", 0)), + "human_player": int(data.get("human_player", 0)), + + # Climb + "climb_type": data.get("climb_type", ""), + "climb_success": bool(data.get("climb_success", False)), + + # Defense + "defense_rating": int(data.get("defense_rating", 1)), + "defense_notes": data.get("defense_notes", ""), + + # Auto + "auto_path": data.get("auto_path", ""), + "auto_notes": data.get("auto_notes", ""), + + # Notes + "notes": data.get("notes", ""), } result = self.db.team_data.update_one( @@ -283,6 +301,81 @@ def has_team_data(self, team_number): logger.error(f"Error checking team data: {str(e)}") return False + @with_mongodb_retry(retries=3, delay=2) + def get_team_stats(self, team_number): + """Get comprehensive stats for a team""" + self.ensure_connected() + try: + pipeline = [ + {"$match": {"team_number": int(team_number)}}, + { + "$group": { + "_id": "$team_number", + "matches_played": {"$sum": 1}, + "total_coral": { + "$sum": { + "$add": [ + "$coral_level1", + "$coral_level2", + "$coral_level3", + "$coral_level4" + ] + } + }, + "total_algae": { + "$sum": {"$add": ["$algae_net", "$algae_processor"]} + }, + "successful_climbs": { + "$sum": {"$cond": ["$climb_success", 1, 0]} + }, + "total_defense": {"$sum": "$defense_rating"}, + "total_points": {"$sum": "$total_points"} + } + } + ] + + result = list(self.db.team_data.aggregate(pipeline)) + if not result: + return { + "matches_played": 0, + "total_coral": 0, + "total_algae": 0, + "successful_climbs": 0, + "total_defense": 0, + "total_points": 0 + } + + stats = result[0] + stats.pop("_id") # Remove MongoDB ID + return stats + except Exception as e: + logger.error(f"Error getting team stats: {str(e)}") + return None + + @with_mongodb_retry(retries=3, delay=2) + def get_team_matches(self, team_number): + """Get all match data for a specific team""" + self.ensure_connected() + try: + pipeline = [ + {"$match": {"team_number": int(team_number)}}, + {"$sort": {"event_code": 1, "match_number": 1}}, + { + "$lookup": { + "from": "users", + "localField": "scouter_id", + "foreignField": "_id", + "as": "scouter" + } + }, + {"$unwind": "$scouter"} + ] + + return list(self.db.team_data.aggregate(pipeline)) + except Exception as e: + logger.error(f"Error getting team matches: {str(e)}") + return [] + def __del__(self): """Cleanup MongoDB connection""" if self.client: diff --git a/app/static/images/field-2025.png b/app/static/images/field-2025.png new file mode 100644 index 0000000000000000000000000000000000000000..487972f1cbda1de53102df39841994a7998e4665 GIT binary patch literal 18593 zcmdVCcTki|w>SFCFk}geAOaEuK_n@XvywrAD1u}`1POvf3BwE^pprxo6c7+3OU@Yq z$vFqfIp-Ya_Tb*9&ij4mR^5NTd+S;?)~;{1s{Wl0BcD9R7NWbL
Vd&uM!Wrd-lTnYs^Q~0%(JHK*6vv#ZM2)Ym-tq9S26`>Vcdd#)`ZGBQ$f z?&a}GH4a%h6WOWP`%6rWiDILT;<3R&LEPLSuT+9xDX<^v!g;QE@QH{V^)2492*|xe zU>>>mGwCHJBsk}-kH4#|W>+0gQ5&+cLHi3~UXvf!Amn+Ue6Y}ZTPEI`iz#WWn< zZ(Y7W=GM3Q#=RG+w9~1ro@n_bRf#L5{a9a{ll+eWLMmq=VN?UG@BXJzaYXxam{QfP zG=?1QpQGs3@e>?MexVC5hRGX?pan|JE0o0aMIH16-q7{70DN9Xn0b;ZC`Xn}~ow zVQkoz3C_amjbRlBVgx4z8d6X2g4FHry*#}5QNzG&=I40D@miT*_&9-5T*Ky@6Frm3A(x|MNIH18? zcYvvQp-m8sP_H=?)T^hiGll?~|hgM z1E%EEw6vnyN{7TI{1d;LsWs{4a*)S+SKTeDzJIi}M$vz%ky*S1#&(dL)6d~A#YyGX z+UPiv7`nIigm5Nh}K@67mZ9G`qoL6@4PQBSTNS*L`IY3k71sykJZ#IE6U)+BN5nQSb zH?hYL9cI_r7=k&Ni?Z=Zye`L`7|oG*a+mDFz@AoJm@&C9IbcbxH^6dU+f}Hep4caQ>$Ilgj~3j&6G%ZRiqcR@Nu0wG&>4D`#FNC$#KW;%=8d(o z^>*MX?l2!p>N?>_`Q58#f+sRUt}=ZU&nAy(ls^wk&!2X`w32y|9%(?Ny;cF z9jZm^DsRQW>TRePUgJ-19(>#Ld&ewgu{qeV_c&seFtq=w(^DDdyIl`-9js2UY8DteYUUU zY0IWL`z(jtRztz5aBkgXkGIrI0(9DTZ4Kr6MNP?BuFWyWn9-|f=0w!hTwmtkck1wEH0X+TSP z057rmD1c`I#Wg67qmj9aNts&LkB)Wmq9mtgQzznproT09k|3mlpw~hKGYZv0m^Gv{ zf!N1{k{v&rq1@k;!42|(zE2Xf-itFr@I|b+nZs+(wF#0}dS9Q>8=P`6X@6@K`rI1$M2g zO{f4ZSnkqQQaFsflMpN*NpP;)xG+($cQ>#BOh@C;sZ-E5_5D%jad9c7RKpRBv*zoD zx8DwM5ZGD4c&oqPcv=xCuxH(ssb6yEg)yhp>`~+SCw3H5ZG~d6Y z2>~~2L)E4N8>SB+R$C$cn}j|c@4!;}NRValGb-4Xll2WYA6~yLAmEf8wsl1=Q|-@PDWyZ!D;uFFTYcm!XowΜj<=7DKCC#TB5#U^IDf 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 - +
-

Match Points

+

Coral Scoring

- + + name="coral_level1" + value="0" + min="0" + class="w-full px-4 py-2 rounded-md border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
- + + name="coral_level2" + value="0" + min="0" + class="w-full px-4 py-2 rounded-md border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
- + + name="coral_level3" + value="0" + min="0" + class="w-full px-4 py-2 rounded-md border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> +
+
+ +
- -
-

Additional Notes

- + +
+

Algae Scoring

+
+
+ + +
+
+ + +
+
+ + +

Number of successful shots made by the human player

+
+
- + +
+

Climb

+
+
+ + +
+
+ +
+
+
+ + +
+

Defense

+
+
+ +
+ + 1 +
+
+
+ + +
+
+
- -
-

Total Points

-

0

+ +
+

Auto Path

+
+ + +
+ +
+

Draw the robot's autonomous path

+ +
+
+ + +
+
+ + +
+

Additional Notes

+
@@ -143,14 +235,6 @@

Total Points

Add Team Data
- - - -
@@ -171,13 +255,36 @@

Total Points

}); function updateTotal() { - const total = pointInputs.reduce((sum, inputName) => { - const value = parseInt(document.querySelector(`input[name="${inputName}"]`).value) || 0; - return sum + value; + const coralPoints = [1, 2, 3, 4].reduce((sum, level) => { + return sum + (parseInt(document.querySelector(`input[name="coral_level${level}"]`).value) || 0) * level; }, 0); - totalPointsDisplay.textContent = total; + + 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 total = coralPoints + algaeNet + algaeProcessor + 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)); + + // Initialize total + updateTotal(); + // Auto-calculate match result const allianceScoreInput = document.querySelector('input[name="alliance_score"]'); const opponentScoreInput = document.querySelector('input[name="opponent_score"]'); @@ -228,5 +335,120 @@

Total Points

} }); }); + +const canvas = document.getElementById('autoPath'); +const ctx = canvas.getContext('2d'); +let isDrawing = false; +let lastX = 0; +let lastY = 0; +let bgImage = new Image(); + +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 after resize + drawBackground(); +} + +function drawBackground() { + if (!bgImage.complete) return; // Wait for image to load + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Calculate scaling to fit while maintaining aspect ratio + const scale = Math.min( + canvas.width / bgImage.width, + canvas.height / bgImage.height + ); + + // Calculate position to center the image + const x = (canvas.width - bgImage.width * scale) / 2; + const y = (canvas.height - bgImage.height * scale) / 2; + + // Draw background + ctx.drawImage(bgImage, x, y, bgImage.width * scale, bgImage.height * scale); +} + +function getPointerPosition(e) { + const rect = canvas.getBoundingClientRect(); + const x = ((e.touches ? e.touches[0].clientX : e.clientX) - rect.left) * (canvas.width / rect.width); + const y = ((e.touches ? e.touches[0].clientY : e.clientY) - rect.top) * (canvas.height / rect.height); + return { x, y }; +} + +function startDrawing(e) { + e.preventDefault(); + isDrawing = true; + const pos = getPointerPosition(e); + lastX = pos.x; + lastY = pos.y; +} + +function draw(e) { + e.preventDefault(); + if (!isDrawing) return; + + const pos = getPointerPosition(e); + ctx.beginPath(); + ctx.strokeStyle = '#FF0000'; + ctx.lineWidth = 2; + ctx.lineCap = 'round'; + ctx.moveTo(lastX, lastY); + ctx.lineTo(pos.x, pos.y); + ctx.stroke(); + + lastX = pos.x; + lastY = pos.y; + + // Save the combined image + document.getElementById('autoPathData').value = canvas.toDataURL(); +} + +function stopDrawing() { + isDrawing = false; +} + +function clearCanvas() { + drawBackground(); + document.getElementById('autoPathData').value = ''; +} + +// Event listeners +canvas.addEventListener('mousedown', startDrawing); +canvas.addEventListener('mousemove', draw); +canvas.addEventListener('mouseup', stopDrawing); +canvas.addEventListener('mouseleave', stopDrawing); + +canvas.addEventListener('touchstart', startDrawing); +canvas.addEventListener('touchmove', draw); +canvas.addEventListener('touchend', stopDrawing); +canvas.addEventListener('touchcancel', stopDrawing); + +// Prevent scrolling while drawing +canvas.addEventListener('touchstart', e => e.preventDefault()); +canvas.addEventListener('touchmove', e => e.preventDefault()); + +// Initialize +window.addEventListener('load', () => { + bgImage.onload = () => { + resizeCanvas(); + }; + bgImage.src = "{{ url_for('static', filename='images/field-2025.png') }}"; +}); +window.addEventListener('resize', resizeCanvas); {% endblock %} \ No newline at end of file diff --git a/app/templates/scouting/edit.html b/app/templates/scouting/edit.html index b21dc0d..5016f7d 100644 --- a/app/templates/scouting/edit.html +++ b/app/templates/scouting/edit.html @@ -7,8 +7,8 @@

Edit Team Data

+
-

Team Information

@@ -20,7 +20,7 @@

Team Information

value="{{ team_data.team_number }}" required min="1" - class="w-full px-4 py-2 rounded-md border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-150"> + class="w-full px-4 py-2 rounded-md border">
@@ -28,7 +28,7 @@

Team Information

name="event_code" value="{{ team_data.event_code }}" required - class="w-full px-4 py-2 rounded-md border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 uppercase transition duration-150"> + class="w-full px-4 py-2 rounded-md border uppercase">
@@ -37,7 +37,7 @@

Team Information

value="{{ team_data.match_number }}" required min="1" - class="w-full px-4 py-2 rounded-md border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-150"> + class="w-full px-4 py-2 rounded-md border">
@@ -50,7 +50,7 @@

Alliance

name="alliance" value="red" {% if team_data.alliance == 'red' %}checked{% endif %} - class="form-radio text-red-600 h-4 w-4"> + class="form-radio text-red-600"> Red Alliance - -
-
-

Match Points

-
-
- - -
-
- - -
-
- - -
+ +
+

Coral Scoring

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

Algae Scoring

+
+
+ +
+
+ + +
+
+ + +

Number of successful shots made by the human player

+
+
+
+ + +
+

Climb

+
+
+ + +
+
+ + +
+
+
+
+ + +
+

Defense

+
+
+ + + {{ team_data.defense_rating }}
+
+ + +
+
+
+ + +
+

Auto Path

+ + + +
+ +
-
+

Additional Notes

-
- - -
-

Total Points

-

0

+ class="w-full px-4 py-2 rounded-md border">{{ team_data.notes }}
+ class="px-6 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"> Cancel
@@ -147,61 +221,54 @@

Total Points

{% endblock %} \ No newline at end of file diff --git a/app/templates/scouting/leaderboard.html b/app/templates/scouting/leaderboard.html index fcd4c02..fdd5e78 100644 --- a/app/templates/scouting/leaderboard.html +++ b/app/templates/scouting/leaderboard.html @@ -14,10 +14,11 @@

Team Leaderboard

Rank Team - Record (W-L-T) - Win Rate Matches - Avg Points + Avg Coral + Avg Algae + Climb % + Defense Total Points @@ -31,20 +32,42 @@

Team Leaderboard

Team {{ team.team_number }} - - {{ team.wins }}-{{ team.losses }}-{{ team.ties }} + {{ team.matches_played }} + +
+
{{ "%.1f"|format(team.avg_coral) }}
+
per match
+
+ + +
+
{{ "%.1f"|format(team.avg_algae) }}
+
per match
+
-
+
+
- {{ "%.1f%%"|format(team.win_rate) }} + {{ "%.1f%%"|format(team.climb_success_rate) }}
- {{ team.matches_played }} - {{ "%.1f"|format(team.avg_points) }} - {{ team.total_points }} + +
+
+
+
+
+ {{ "%.1f"|format(team.avg_defense) }}/5 +
+ + + {{ team.total_points }} + {% endfor %} diff --git a/app/templates/scouting/list.html b/app/templates/scouting/list.html index 94c7815..685b866 100644 --- a/app/templates/scouting/list.html +++ b/app/templates/scouting/list.html @@ -29,6 +29,31 @@

Pending Entries

+ + +

Team Data

@@ -98,30 +123,33 @@

Team # - + + Alliance + + Match - - Auto + + Coral (Level 1-4) - - Teleop + + Algae (Net/Processor/Human Player) - - Endgame + + Climb + + + Auto Path - Total + Defense - + Notes Scouter - - Alliance - Actions @@ -136,16 +164,52 @@

data-scouter="{{ data.scouter_name }}"> - + {{ data.team_number }} + +
+ + {{ data.alliance|title }} + +
+ {{ data.match_number }} - {{ data.auto_points }} - {{ data.teleop_points }} - {{ data.endgame_points }} - {{ data.total_points }} + + {{ data.coral_level1 }}/{{ data.coral_level2 }}/{{ data.coral_level3 }}/{{ data.coral_level4 }} + + + {{ data.algae_net }}/{{ data.algae_processor }}/{{ data.human_player }} + + + + {% if data.climb_success %} + βœ“ {{ data.climb_type }} + {% else %} + βœ— {{ data.climb_type }} + {% endif %} + + + {% if data.auto_path %} + + {% else %} + No path + {% endif %} + + + {{ data.defense_rating }}/5 + {{ data.notes }}
@@ -161,18 +225,6 @@

- -
- - {{ data.alliance|title }} - -
-
{% if data.scouter_id|string == current_user.id|string %} @@ -245,5 +297,33 @@

searchInput.addEventListener('input', filterRows); filterType.addEventListener('change', filterRows); }); + + function showAutoPath(pathData, autoNotes = '') { + const modal = document.getElementById('autoPathModal'); + const image = document.getElementById('modalAutoPathImage'); + const notes = document.getElementById('modalAutoNotes'); + + image.src = pathData; + notes.textContent = autoNotes || 'No auto notes provided'; + modal.classList.remove('hidden'); + } + + function closeAutoPathModal() { + document.getElementById('autoPathModal').classList.add('hidden'); + } + + // Close modal when clicking outside + document.getElementById('autoPathModal').addEventListener('click', function(e) { + if (e.target === this) { + closeAutoPathModal(); + } + }); + + // Close modal with Escape key + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + closeAutoPathModal(); + } + }); {% endblock %} \ No newline at end of file diff --git a/app/templates/scouting/matches.html b/app/templates/scouting/matches.html index 8c46792..ee8e0be 100644 --- a/app/templates/scouting/matches.html +++ b/app/templates/scouting/matches.html @@ -22,7 +22,7 @@

Match List

Blue Alliance - Final Score + Details @@ -35,36 +35,55 @@

Match List

-
+
{% for team in match.red_teams %}
+ class="text-red-600 hover:text-red-800 font-medium"> {{ team.number }} - - {{ team.total_points }} points +
+ Coral: {{ team.coral_total }} | + Algae: {{ team.algae_total }} | + {% if team.climb_success %} + βœ“ {{ team.climb_type }} + {% endif %} +
{% endfor %}
-
+
{% for team in match.blue_teams %}
+ class="text-blue-600 hover:text-blue-800 font-medium"> {{ team.number }} - - {{ team.total_points }} points +
+ Coral: {{ team.coral_total }} | + Algae: {{ team.algae_total }} | + {% if team.climb_success %} + βœ“ {{ team.climb_type }} + {% endif %} +
{% endfor %}
-
- {{ match.red_score }} - - - {{ match.blue_score }} +
+
+ {{ match.red_score }} + - + {{ match.blue_score }} +
+
+ Red: {{ match.red_coral_total }} coral, {{ match.red_algae_total }} algae +
+ Blue: {{ match.blue_coral_total }} coral, {{ match.blue_algae_total }} algae +
diff --git a/app/templates/scouting/team.html b/app/templates/scouting/team.html index 0a504d7..fdaae39 100644 --- a/app/templates/scouting/team.html +++ b/app/templates/scouting/team.html @@ -51,16 +51,16 @@

Average Points

{{ (stats.total_points / stats.matches_played) | round(1) }}

-

Avg Auto

-

{{ stats.auto_points | round(1) }}

+

Avg Coral

+

{{ stats.avg_coral | round(1) }}

-

Avg Teleop

-

{{ stats.teleop_points | round(1) }}

+

Avg Algae

+

{{ stats.avg_algae | round(1) }}

-

Avg Endgame

-

{{ stats.endgame_points | round(1) }}

+

Climb Success

+

{{ (stats.climb_success_rate * 100) | round(1) }}%

@@ -80,9 +80,9 @@

Match History

Match - Auto - Teleop - Endgame + Coral (L1-L4) + Algae (Net/Proc) + Climb Total Notes @@ -91,9 +91,20 @@

Match History

{% for match in matches %} {{ match.event_code }} - {{ match.match_number }} - {{ match.auto_points }} - {{ match.teleop_points }} - {{ match.endgame_points }} + + {{ match.coral_level1 }}/{{ match.coral_level2 }}/{{ match.coral_level3 }}/{{ match.coral_level4 }} + + + {{ match.algae_net }}/{{ match.algae_processor }} + {% if match.human_player %}βœ“ HP{% endif %} + + + {% if match.climb_success %} + βœ“ {{ match.climb_type }} + {% else %} + βœ— + {% endif %} + {{ match.total_points }} {{ match.notes }} @@ -192,14 +203,14 @@

Match History

const consistency = calculateConsistency(matchesData); const radarData = { - labels: ['Auto', 'Teleop', 'Endgame', 'Consistency', 'Overall'], + labels: ['Coral', 'Algae', 'Climb', 'Consistency', 'Defense'], datasets: [{ data: [ - {{ stats.auto_points|float }}, - {{ stats.teleop_points|float }}, - {{ stats.endgame_points|float }}, + {{ stats.avg_coral|float }}, + {{ stats.avg_algae|float }}, + {{ (stats.climb_success_rate * 100)|float }}, consistency, - {{ (stats.total_points / stats.matches_played)|float }} + {{ stats.avg_defense|float }} ], backgroundColor: 'rgba(54, 162, 235, 0.2)', borderColor: 'rgb(54, 162, 235)',