diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..f2d7cde 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/planningMode.xml b/.idea/planningMode.xml new file mode 100644 index 0000000..05fc08a --- /dev/null +++ b/.idea/planningMode.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d899069..0ece391 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { implementation(libs.material) implementation(libs.activity) implementation(libs.constraintlayout) + implementation(libs.mpandroidchart) testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) diff --git a/app/release/app-release.apk b/app/release/app-release.apk index ad9b2cf..02a6653 100644 Binary files a/app/release/app-release.apk and b/app/release/app-release.apk differ diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm index ecc351b..157bea5 100644 Binary files a/app/release/baselineProfiles/0/app-release.dm and b/app/release/baselineProfiles/0/app-release.dm differ diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm index 6e10463..8fb9fa3 100644 Binary files a/app/release/baselineProfiles/1/app-release.dm and b/app/release/baselineProfiles/1/app-release.dm differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9631586..7f7c15f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/roboticsscoutingmatchapp/ActivityCompetitionSelection.java b/app/src/main/java/com/example/roboticsscoutingmatchapp/ActivityCompetitionSelection.java index df07a7c..2c535fa 100644 --- a/app/src/main/java/com/example/roboticsscoutingmatchapp/ActivityCompetitionSelection.java +++ b/app/src/main/java/com/example/roboticsscoutingmatchapp/ActivityCompetitionSelection.java @@ -4,16 +4,22 @@ import android.content.Intent; import android.os.Bundle; import android.util.Log; +import android.view.View; import android.widget.Button; -import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.Toast; import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; import androidx.core.graphics.Insets; +import androidx.core.view.GravityCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import androidx.drawerlayout.widget.DrawerLayout; + +import com.google.android.material.navigation.NavigationView; import java.io.BufferedReader; import java.io.File; @@ -24,6 +30,10 @@ import java.io.InputStreamReader; import java.util.Calendar; +/** + * Launcher activity for the app. Handles competition selection and data persistence checks. + * Serves as the home screen with navigation to the Data Dashboard. + */ public class ActivityCompetitionSelection extends AppCompatActivity { @Override @@ -31,19 +41,45 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EdgeToEdge.enable(this); setContentView(R.layout.activity_competition_selection); - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + + // Sidebar/Navigation Drawer Setup + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + DrawerLayout drawer = findViewById(R.id.drawer_layout); + NavigationView navigationView = findViewById(R.id.nav_view); + + // Adjust for system status bars + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.app_bar), (v, insets) -> { Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + v.setPadding(0, systemBars.top, 0, 0); return insets; }); + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( + this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); + drawer.addDrawerListener(toggle); + toggle.syncState(); + + // Centralized navigation logic + navigationView.setNavigationItemSelectedListener(item -> { + int id = item.getItemId(); + if (id == R.id.nav_dashboard) { + // Open Data Dashboard + startActivity(new Intent(this, activityDataShowing.class)); + } else if (id == R.id.nav_scout) { + // Reset/Restart Scouting flow + Intent intent = new Intent(this, ActivityCompetitionSelection.class); + intent.putExtra("chooseNewCompetition", true); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } + drawer.closeDrawer(GravityCompat.START); + return true; + }); + U u = new U(); RadioGroup competitionRadioGroup = findViewById(R.id.competition_radio_group); -// RadioButton wpiButton = findViewById(R.id.comp_wpi_button); -// RadioButton uvmButton = findViewById(R.id.comp_uvm_button); -// RadioButton dcmpButton = findViewById(R.id.comp_dcmp); -// RadioButton worldsButton = findViewById(R.id.comp_worlds); -// RadioButton testButton = findViewById(R.id.comp_test); Button saveButton = findViewById(R.id.save_button); Toast unfilledMessage = new Toast(this); unfilledMessage.setDuration(Toast.LENGTH_SHORT); @@ -58,6 +94,7 @@ protected void onCreate(Bundle savedInstanceState) { scoutName = ""; } + // Persistence check: If a competition was already selected today, skip this screen final String FILENAME = "matchAndDate"; File file = new File(this.getFilesDir(), FILENAME); Calendar now = Calendar.getInstance(); @@ -76,14 +113,15 @@ protected void onCreate(Bundle savedInstanceState) { dateAndMatchString += reader.readLine(); reader.close(); } catch (FileNotFoundException e) { - Log.e("File Not Found Exception", e.toString()); + Log.e("ActivityComp", "Persisted file not found", e); } catch (IOException e) { - Log.e("IOException", e.toString()); + Log.e("ActivityComp", "Read error", e); } } if (!dateAndMatchString.isEmpty()) { String fileDate = u.untilNextComma(dateAndMatchString); if (fileDate.equals(currentDate)) { + // Fast-forward to scouting if date matches Intent i = new Intent(this, activityPreMatch.class); i.putExtra("competition", u.nextCommaOn(dateAndMatchString)); i.putExtra("scoutName", scoutName); @@ -98,14 +136,15 @@ protected void onCreate(Bundle savedInstanceState) { if(u.getData(competitionRadioGroup).isEmpty()){ response = "Please choose current competition to be scouting"; }else{ + // Save selection locally String fileContents = finalCurrentDate + "," + u.getData(competitionRadioGroup); try(FileOutputStream fos = this.openFileOutput(FILENAME, Context.MODE_PRIVATE)){ fos.write(fileContents.getBytes()); - } catch(FileNotFoundException e){ - Log.e("File Not Found Exception", e.toString()); } catch(IOException e){ - Log.e("IO Exception", e.toString()); + Log.e("ActivityComp", "Write error", e); } + + // Move to Pre-Match Intent i = new Intent(this, activityPreMatch.class); i.putExtra("competition", u.getData(competitionRadioGroup)); i.putExtra("scoutName", scoutName); @@ -117,4 +156,15 @@ protected void onCreate(Bundle savedInstanceState) { } }); } -} \ No newline at end of file + + @Override + public void onBackPressed() { + // Handle Sidebar Drawer if open + DrawerLayout drawer = findViewById(R.id.drawer_layout); + if (drawer != null && drawer.isDrawerOpen(GravityCompat.START)) { + drawer.closeDrawer(GravityCompat.START); + } else { + super.onBackPressed(); + } + } +} diff --git a/app/src/main/java/com/example/roboticsscoutingmatchapp/MatchData.java b/app/src/main/java/com/example/roboticsscoutingmatchapp/MatchData.java new file mode 100644 index 0000000..8dd9027 --- /dev/null +++ b/app/src/main/java/com/example/roboticsscoutingmatchapp/MatchData.java @@ -0,0 +1,159 @@ +package com.example.roboticsscoutingmatchapp; + +/** + * Data model representing a single match scouting report for the 2026 REBUILT game. + * Maps directly to the 29-column CSV schema used for data aggregation. + */ +public class MatchData { + // Basic Match Info + public String competition; + public int dataVersion; // Incrementing version to track schema changes (e.g., 2026 = v2) + public String scout; + public String teamNum; + public String teamColor; + public String matchNum; + + // Pre-Match + public boolean preloadedFuel; + + // Autonomous Period + public String startingPos; + public int autoScored; // Total fuel shots fired in Auto + public String autoAccuracy; // Qualitative accuracy percentage (e.g., "75%") + public int autoPassed; // Fuel passed to alliance partners + public boolean autoHang; // Successful climb to Tower Level 1 during Auto + + // TeleOp Period + public int teleopScored; // Total fuel shots fired in TeleOp + public String teleopAccuracy; + public int teleopPassed; + public String hangStatus; // Endgame climb level (Level 1, 2, 3, or None) + public int hangTime; // Time remaining when climb was completed + public String accuracyPos; // Movement state with best accuracy + + // Field Navigation & Robot State (2026 specific) + public boolean overBump; + public boolean underTrench; + public boolean playedDefense; + public boolean collectedFuel; + public boolean passedFuel; + public boolean didNothing; + public boolean other; + + // Post-Match Qualitative Data + public String defense; // Description of defense received/given + public String stopReason; // Reason if the robot stopped moving + public String rank; // Relative rank in the alliance + public String comments; + + /** + * Constructs a MatchData object from a raw CSV row. + * @param csvRow String array containing the 29 values in the schema order. + */ + public MatchData(String[] csvRow) { + if (csvRow.length < 29) return; + + this.competition = csvRow[0]; + this.dataVersion = tryParseInt(csvRow[1]); + this.scout = csvRow[2]; + this.teamNum = csvRow[3]; + this.teamColor = csvRow[4]; + this.matchNum = csvRow[5]; + this.preloadedFuel = Boolean.parseBoolean(csvRow[6]); + this.startingPos = csvRow[7]; + this.autoScored = tryParseInt(csvRow[8]); + this.autoAccuracy = csvRow[9]; + this.autoPassed = tryParseInt(csvRow[10]); + this.autoHang = Boolean.parseBoolean(csvRow[11]); + this.teleopScored = tryParseInt(csvRow[12]); + this.teleopAccuracy = csvRow[13]; + this.teleopPassed = tryParseInt(csvRow[14]); + this.hangStatus = csvRow[15]; + this.hangTime = tryParseInt(csvRow[16]); + this.accuracyPos = csvRow[17]; + this.overBump = Boolean.parseBoolean(csvRow[18]); + this.underTrench = Boolean.parseBoolean(csvRow[19]); + this.playedDefense = Boolean.parseBoolean(csvRow[20]); + this.collectedFuel = Boolean.parseBoolean(csvRow[21]); + this.passedFuel = Boolean.parseBoolean(csvRow[22]); + this.didNothing = Boolean.parseBoolean(csvRow[23]); + this.other = Boolean.parseBoolean(csvRow[24]); + this.defense = csvRow[25]; + this.stopReason = csvRow[26]; + this.rank = csvRow[27]; + this.comments = csvRow[28]; + } + + /** + * Estimates the number of fuel pieces scored in Auto. + * Calculation: Total Shots * Accuracy Multiplier + */ + public int getCalculatedAutoScoredWhole() { + return (int) Math.round(autoScored * getAccuracyMultiplier(autoAccuracy)); + } + + /** + * Estimates the number of fuel pieces scored in TeleOp. + */ + public int getCalculatedTeleopScoredWhole() { + return (int) Math.round(teleopScored * getAccuracyMultiplier(teleopAccuracy)); + } + + /** + * Calculates total points earned in Autonomous. + * Includes fuel points and the 15pt Auto Climb bonus. + */ + public double getCalculatedAutoScore() { + return (double) getCalculatedAutoScoredWhole() + (autoHang ? 15 : 0); + } + + /** + * Calculates total points earned in TeleOp/Endgame. + * Includes fuel points and Tower climb points (10, 20, or 30). + */ + public double getCalculatedTeleopScore() { + return (double) getCalculatedTeleopScoredWhole() + getClimbPoints(); + } + + /** + * Total match score contribution by this robot. + */ + public double getTotalScore() { + return getCalculatedAutoScore() + getCalculatedTeleopScore(); + } + + /** + * Maps the hangStatus string to specific point values for the 2026 REBUILT game. + */ + public int getClimbPoints() { + if (hangStatus == null) return 0; + switch (hangStatus) { + case "Level 1": return 10; + case "Level 2": return 20; + case "Level 3": return 30; + default: return 0; + } + } + + /** + * Converts qualitative accuracy strings to numeric multipliers. + * Note: "More than 95%" is treated as 0.97 to account for occasional misses. + */ + private double getAccuracyMultiplier(String accuracy) { + if (accuracy == null) return 0; + if (accuracy.contains("10%")) return 0.05; + if (accuracy.contains("25%")) return 0.25; + if (accuracy.contains("50%")) return 0.50; + if (accuracy.contains("75%")) return 0.75; + if (accuracy.contains("95%")) return 0.97; + return 0; + } + + private int tryParseInt(String val) { + try { + return Integer.parseInt(val.trim()); + } catch (Exception e) { + return 0; + } + } +} diff --git a/app/src/main/java/com/example/roboticsscoutingmatchapp/MatchHistoryAdapter.java b/app/src/main/java/com/example/roboticsscoutingmatchapp/MatchHistoryAdapter.java new file mode 100644 index 0000000..e9bad4e --- /dev/null +++ b/app/src/main/java/com/example/roboticsscoutingmatchapp/MatchHistoryAdapter.java @@ -0,0 +1,93 @@ +package com.example.roboticsscoutingmatchapp; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +/** + * Adapter for displaying a list of MatchData objects in a RecyclerView. + * Used in the Data Hub Dashboard to show match history for a specific team. + */ +public class MatchHistoryAdapter extends RecyclerView.Adapter { + + /** + * Listener interface for match selection events. + */ + public interface OnMatchClickListener { + void onMatchClick(MatchData match); + } + + private List matches; + private OnMatchClickListener listener; + + public MatchHistoryAdapter(List matches, OnMatchClickListener listener) { + this.matches = matches; + this.listener = listener; + } + + /** + * Updates the dataset and refreshes the UI list. + */ + public void updateData(List newMatches) { + this.matches = newMatches; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_match_history, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + MatchData match = matches.get(position); + + // Display summary info for the match row + holder.matchNum.setText("M #" + match.matchNum); + + // Use calculated pieces scored (Shots * Accuracy) for the summary text + holder.scoreSummary.setText(String.format("Auto: %d | Tele: %d", + match.getCalculatedAutoScoredWhole(), + match.getCalculatedTeleopScoredWhole())); + + holder.hangStatus.setText("Climb: " + match.hangStatus); + holder.scouterName.setText(match.scout); + + // Click listener to open the detailed match popup + holder.itemView.setOnClickListener(v -> { + if (listener != null) { + listener.onMatchClick(match); + } + }); + } + + + @Override + public int getItemCount() { + return matches.size(); + } + + /** + * ViewHolder for the match history list items. + */ + public static class ViewHolder extends RecyclerView.ViewHolder { + TextView matchNum, scoreSummary, hangStatus, scouterName; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + matchNum = itemView.findViewById(R.id.match_num_text); + scoreSummary = itemView.findViewById(R.id.score_summary_text); + hangStatus = itemView.findViewById(R.id.hang_status_text); + scouterName = itemView.findViewById(R.id.scout_name_text); + } + } +} diff --git a/app/src/main/java/com/example/roboticsscoutingmatchapp/U.java b/app/src/main/java/com/example/roboticsscoutingmatchapp/U.java index 5cfb3e1..b52fc04 100644 --- a/app/src/main/java/com/example/roboticsscoutingmatchapp/U.java +++ b/app/src/main/java/com/example/roboticsscoutingmatchapp/U.java @@ -21,7 +21,7 @@ public class U extends AppCompatActivity{ - public final int DATA_VERSION = 1; + public final int DATA_VERSION = 2; // Characters to strip in input fields for stripText method public static final String [] DELIMITER = {",", ";", ":", "|", "\n"}; public static final String [] WHITESPACE = {" "}; diff --git a/app/src/main/java/com/example/roboticsscoutingmatchapp/activityAfterMatch.java b/app/src/main/java/com/example/roboticsscoutingmatchapp/activityAfterMatch.java index 2f1d30f..bdaec47 100644 --- a/app/src/main/java/com/example/roboticsscoutingmatchapp/activityAfterMatch.java +++ b/app/src/main/java/com/example/roboticsscoutingmatchapp/activityAfterMatch.java @@ -22,23 +22,26 @@ import java.io.FileWriter; import java.io.IOException; +/** + * Activity for collecting Post-Match qualitative data (Field navigation, robot state, stop reason). + * Finalizes and saves the scouting report as a CSV file. + */ public class activityAfterMatch extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); // Default code start + super.onCreate(savedInstanceState); EdgeToEdge.enable(this); setContentView(R.layout.activity_after_match); ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); return insets; - }); // Default code end - - U u = new U(); // defines new utilities object to be used + }); - // *---Defines all the components on the current page as variables---* + U u = new U(); + // 2026 REBUILT Metric Collection CheckBox overBump = findViewById(R.id.over_bump); CheckBox underTrench = findViewById(R.id.under_trench); CheckBox playedDefense = findViewById(R.id.played_defense); @@ -67,28 +70,30 @@ protected void onCreate(Bundle savedInstanceState) { EditText finalText = findViewById(R.id.End_of_app_text); Button saveButton = findViewById(R.id.save_button); Button backButton = findViewById(R.id.back_button); - Toast unfilledMessage = new Toast(this); // Creates a Toast (pop-up message object) - unfilledMessage.setDuration(Toast.LENGTH_SHORT); // Sets it to only be angry a short while + + Toast unfilledMessage = new Toast(this); + unfilledMessage.setDuration(Toast.LENGTH_SHORT); - String preMatchSaveString, autoSaveString, // Define savestring String objects + // Load intent data for UI state persistence + String preMatchSaveString, autoSaveString, teleOpSaveString, postMatchSaveString; - Bundle extras = getIntent().getExtras(); // Gets the savestrings and stores them to extras - if(extras != null){ // If savestrings exist, set individual strings to values + Bundle extras = getIntent().getExtras(); + if(extras != null){ preMatchSaveString = extras.getString("preMatch", ""); autoSaveString = extras.getString("auto", ""); teleOpSaveString = extras.getString("teleOp", ""); postMatchSaveString = extras.getString("postMatch", ""); - } else { // If savestrings don't exist, no value + } else { preMatchSaveString = ""; autoSaveString = ""; teleOpSaveString = ""; postMatchSaveString = ""; } - if(!postMatchSaveString.isEmpty()){ // Sets all the components to the values within the savestring - overBump.setChecked(Boolean.parseBoolean(u.untilNextComma(postMatchSaveString))); // Sets the value to the parsed value in the savestring - postMatchSaveString = u.nextCommaOn(postMatchSaveString); // removes the value from the savestring - // So on an so forth + // Parse backward-passed post-match data strings to restore UI state + if(!postMatchSaveString.isEmpty()){ + overBump.setChecked(Boolean.parseBoolean(u.untilNextComma(postMatchSaveString))); + postMatchSaveString = u.nextCommaOn(postMatchSaveString); underTrench.setChecked(Boolean.parseBoolean(u.untilNextComma(postMatchSaveString))); postMatchSaveString = u.nextCommaOn(postMatchSaveString); playedDefense.setChecked(Boolean.parseBoolean(u.untilNextComma(postMatchSaveString))); @@ -113,35 +118,19 @@ protected void onCreate(Bundle savedInstanceState) { String stopReasonString = u.untilNextComma(postMatchSaveString); switch (stopReasonString) { - case "Died": - diedButton.toggle(); - break; - case "Tipped": - tippedButton.toggle(); - break; - case "Physically Broke": - physicallyBrokeButton.toggle(); - break; - case "E-stopped": - eStoppedButton.toggle(); - break; - case "Not Stopped": - notStoppedButton.toggle(); - break; + case "Died": diedButton.toggle(); break; + case "Tipped": tippedButton.toggle(); break; + case "Physically Broke": physicallyBrokeButton.toggle(); break; + case "E-stopped": eStoppedButton.toggle(); break; + case "Not Stopped": notStoppedButton.toggle(); break; } postMatchSaveString = u.nextCommaOn(postMatchSaveString); String teamRank = u.untilNextComma(postMatchSaveString); switch(teamRank){ - case "Rank 1": - rank1Button.toggle(); - break; - case "Rank 2": - rank2Button.toggle(); - break; - case "Rank 3": - rank3Button.toggle(); - break; + case "Rank 1": rank1Button.toggle(); break; + case "Rank 2": rank2Button.toggle(); break; + case "Rank 3": rank3Button.toggle(); break; } postMatchSaveString = u.nextCommaOn(postMatchSaveString); @@ -149,12 +138,11 @@ protected void onCreate(Bundle savedInstanceState) { postMatchSaveString = u.nextCommaOn(postMatchSaveString); } - backButton.setOnClickListener((l)->{ // Sets current savestring to current values of components - // Because it's the back button, these values can have no value + backButton.setOnClickListener((l)->{ String afterMatchInfo = ""; - - afterMatchInfo += u.getData(underTrench) + ","; + // Maintain data integrity for the 29-column schema even on back-nav afterMatchInfo += u.getData(overBump) + ","; + afterMatchInfo += u.getData(underTrench) + ","; afterMatchInfo += u.getData(playedDefense) + ","; afterMatchInfo += u.getData(collectedFuel) + ","; afterMatchInfo += u.getData(passedFuel) + ","; @@ -170,20 +158,18 @@ protected void onCreate(Bundle savedInstanceState) { i.putExtra("auto", autoSaveString); i.putExtra("teleOp", teleOpSaveString); i.putExtra("postMatch", afterMatchInfo); - this.startActivity(i); }); - saveButton.setOnClickListener((l)->{ // Sets current savestring to current component values - // Checks that components that need to be filled in are filled in + saveButton.setOnClickListener((l)->{ String response = ""; - if(u.getData(defenseReceivedGroup).isEmpty()) // If a component is not filled in - response = "Please fill in defense received"; // Sets the toast message to error message + if(u.getData(defenseReceivedGroup).isEmpty()) + response = "Please fill in defense received"; else if(u.getData(rankGroup).isEmpty()) response = "Please fill in rank"; else if(u.getData(stopReasonGroup).isEmpty()) response = "Please fill in stop reason"; - else{ // If nothing is wrong, keep filling everything in + else{ String postMatchInfo = ""; postMatchInfo += u.getData(overBump) + ","; postMatchInfo += u.getData(underTrench) + ","; @@ -197,44 +183,35 @@ else if(u.getData(stopReasonGroup).isEmpty()) postMatchInfo += u.getData(rankGroup) + ","; postMatchInfo += u.stripText(u.getData(finalText), u.DELIMITER) + ","; - // Competition Location | Save Version | Scout Name | Team # | Team Color | - // Match Number | Preloaded coral || - // Filename format: "match_scouting_" + [team_number] + [match number] + // Compile final CSV string and write to external storage (Documents directory) + // Filename format: match_scouting_[comp]_[team]_[match].csv String teamNumber = u.untilNextComma(u.nextCommaOn(u.nextCommaOn(u.nextCommaOn(preMatchSaveString)))); String matchNumber = u.untilNextComma(u.nextCommaOn(u.nextCommaOn(u.nextCommaOn(u.nextCommaOn(u.nextCommaOn(preMatchSaveString)))))); String competitionLocation = u.untilNextComma(preMatchSaveString); String scoutName = u.untilNextComma(u.nextCommaOn(u.nextCommaOn(preMatchSaveString))); String fileName = "match_scouting_"+ competitionLocation + "_" + teamNumber + "_" + matchNumber + ".csv"; - Log.d("File Name: ", fileName); try { File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), fileName); if (!file.exists()) { - boolean fileCreated = file.createNewFile(); - if(!fileCreated) { - Toast.makeText(this, "File " + fileName + " has not been created", Toast.LENGTH_SHORT).show(); - Log.d("File written: ", "File not created"); - }else { - Toast.makeText(this, "File " + fileName + " has been created", Toast.LENGTH_SHORT).show(); - Log.d("File written: ", "File created"); - } - }else{ - Log.d("File already existed", "File already existed"); + file.createNewFile(); } FileWriter fw = new FileWriter(file.getAbsoluteFile()); BufferedWriter bw = new BufferedWriter(fw); - + + // Final Write: Pre + Auto + Tele + Post = 29 Columns bw.write(preMatchSaveString); bw.write(autoSaveString); bw.write(teleOpSaveString); bw.write(postMatchInfo); bw.flush(); bw.close(); + Toast.makeText(this, "File " + fileName + " Saved", Toast.LENGTH_SHORT).show(); } catch (IOException e) { - Log.d("Error thrown:", "+==========+Error Thrown+==========+"); - Log.getStackTraceString(e); + Log.e("activityAfterMatch", "File save failed", e); } + Intent i = new Intent(this, ActivityCompetitionSelection.class); i.putExtra("scoutName", scoutName); this.startActivity(i); @@ -246,4 +223,4 @@ else if(u.getData(stopReasonGroup).isEmpty()) } }); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/roboticsscoutingmatchapp/activityAutonomous.java b/app/src/main/java/com/example/roboticsscoutingmatchapp/activityAutonomous.java index 2578bd6..df7dc25 100644 --- a/app/src/main/java/com/example/roboticsscoutingmatchapp/activityAutonomous.java +++ b/app/src/main/java/com/example/roboticsscoutingmatchapp/activityAutonomous.java @@ -20,8 +20,16 @@ import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +/** + * Activity for collecting Autonomous period data. + * Tracks starting position, fuel shots fired, accuracy, and auto climb. + */ public class activityAutonomous extends AppCompatActivity { + /** + * Helper to clear a RadioGroup when a button in another group is selected. + * Used for multi-column starting position layouts. + */ public void clearGroup(RadioGroup field1, RadioGroup field2){ field1.setOnCheckedChangeListener(null); field1.check(-1); @@ -31,9 +39,9 @@ public void clearGroup(RadioGroup field1, RadioGroup field2){ @Override protected void onCreate(Bundle savedInstanceState) { - U u = new U(); + // Load existing match state String preMatchSaveString, autoSaveString, teleOpSaveString, postMatchSaveString; Bundle extras = getIntent().getExtras(); @@ -59,11 +67,13 @@ protected void onCreate(Bundle savedInstanceState) { return insets; }); + // UI Binding - Starting Position RadioGroup positionGroup1 = findViewById(R.id.staring_position_radio_group1); RadioButton position1Button = findViewById(R.id.Position_1); RadioButton position2Button = findViewById(R.id.Position_2); RadioButton position3Button = findViewById(R.id.Position_3); + // UI Binding - Fuel Shot Counters (FS) Button FS1plus = findViewById(R.id.up_count_button_fs1); Button FS5plus = findViewById(R.id.up_count_button_fs5); Button FS10plus = findViewById(R.id.up_count_button_fs10); @@ -76,6 +86,7 @@ protected void onCreate(Bundle savedInstanceState) { Button FS15minus = findViewById(R.id.down_count_button_fs15); Button FS20minus = findViewById(R.id.down_count_button_fs20); + // UI Binding - Fuel Passing Counters (FP) Button FP1plus = findViewById(R.id.up_count_button_fp1); Button FP5plus = findViewById(R.id.up_count_button_fp5); Button FP10plus = findViewById(R.id.up_count_button_fp10); @@ -91,6 +102,7 @@ protected void onCreate(Bundle savedInstanceState) { EditText FSField = findViewById(R.id.edit_text_fs); EditText FPField = findViewById(R.id.edit_text_fp); + // UI Binding - Accuracy & Performance RadioGroup accuracyGroup = findViewById(R.id.accuracy_radio_group); RadioButton lessThanTen = findViewById(R.id.underTen); RadioButton twentyFivePercent = findViewById(R.id.twentyFivePercent); @@ -98,49 +110,32 @@ protected void onCreate(Bundle savedInstanceState) { RadioButton seventyFivePercent = findViewById(R.id.seventyFivePercent); RadioButton overNinetyFive = findViewById(R.id.overNinetyFive); + CheckBox autoHang = findViewById(R.id.checkBox_auto_hang); + Button backButton = findViewById(R.id.back_button); Button saveButton = findViewById(R.id.save_button); - CheckBox autoHang = findViewById(R.id.checkBox_auto_hang); - Toast unfilledMessage = new Toast(this); unfilledMessage.setDuration(Toast.LENGTH_SHORT); + // Restore UI state from saved data string if(!autoSaveString.isEmpty()){ - // Starting Position | #Shots Fired | Accuracy % | #Fuel Passed | Auto Hang | String position = u.untilNextComma(autoSaveString); -// Log.d(position, position); switch (position){ - case "Position 1": - position1Button.toggle(); - break; - case "Position 2": - position2Button.toggle(); - break; - case "Position 3": - position3Button.toggle(); - break; + case "Position 1": position1Button.toggle(); break; + case "Position 2": position2Button.toggle(); break; + case "Position 3": position3Button.toggle(); break; } autoSaveString = u.nextCommaOn(autoSaveString); String accuracyChoice = u.untilNextComma(autoSaveString); switch(accuracyChoice){ - case "Less Than 10%": - lessThanTen.toggle(); - break; - case "25%": - twentyFivePercent.toggle(); - break; - case "50%": - fiftyPercent.toggle(); - break; - case "75%": - seventyFivePercent.toggle(); - break; - case "More Than 95%": - overNinetyFive.toggle(); - break; + case "Less Than 10%": lessThanTen.toggle(); break; + case "25%": twentyFivePercent.toggle(); break; + case "50%": fiftyPercent.toggle(); break; + case "75%": seventyFivePercent.toggle(); break; + case "More Than 95%": overNinetyFive.toggle(); break; } autoSaveString = u.nextCommaOn(autoSaveString); @@ -148,6 +143,7 @@ protected void onCreate(Bundle savedInstanceState) { autoSaveString = u.nextCommaOn(autoSaveString); } + // Setup Increment/Decrement Listeners FS1plus.setOnClickListener((l)->u.incrementText(FSField)); FS1minus.setOnClickListener((l)->u.incrementText(FSField, -1)); FS5plus.setOnClickListener((l)->u.incrementText(FSField, +5)); @@ -169,19 +165,15 @@ protected void onCreate(Bundle savedInstanceState) { FP15minus.setOnClickListener((l)->u.incrementText(FPField, -15)); FP20plus.setOnClickListener((l)->u.incrementText(FPField, +20)); FP20minus.setOnClickListener((l)->u.incrementText(FPField, -20)); - // Sets all the buttons to either increment or decrement their respective buttons. backButton.setOnClickListener((l)-> { - // Starting Position | #Shots Fired | Accuracy % | #Fuel Passed | Auto Hang | String autoInfo = ""; autoInfo += u.getData(positionGroup1); - autoInfo += u.getData(FSField); autoInfo += u.getData(accuracyGroup); - autoInfo += u.getData(FPField); - autoInfo += u.getData(autoHang); + Intent i = new Intent(this, activityPreMatch.class); i.putExtra("preMatch", preMatchSaveString); i.putExtra("auto", autoInfo); @@ -194,22 +186,17 @@ protected void onCreate(Bundle savedInstanceState) { saveButton.setOnClickListener((l)-> { String response = ""; - if((u.getData(positionGroup1).isEmpty())) response = "Please fill position"; if ((u.getData(accuracyGroup).isEmpty())) response = "Please give accuracy"; else{ - + // Serialize auto data String autoInfo = ""; - autoInfo += u.getData(positionGroup1) + ","; - autoInfo += u.getData(FSField) + ","; autoInfo += u.getData(accuracyGroup) + ","; - autoInfo += u.getData(FPField) + ","; - autoInfo += u.getData(autoHang) + ","; Intent i = new Intent(this, activityTeleOp.class); @@ -227,4 +214,4 @@ protected void onCreate(Bundle savedInstanceState) { } }); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/roboticsscoutingmatchapp/activityDataShowing.java b/app/src/main/java/com/example/roboticsscoutingmatchapp/activityDataShowing.java index dd87fd3..ba645a2 100644 --- a/app/src/main/java/com/example/roboticsscoutingmatchapp/activityDataShowing.java +++ b/app/src/main/java/com/example/roboticsscoutingmatchapp/activityDataShowing.java @@ -1,69 +1,474 @@ package com.example.roboticsscoutingmatchapp; +import android.content.Intent; import android.os.Bundle; import android.os.Environment; import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; +import androidx.core.view.GravityCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.mikephil.charting.charts.HorizontalBarChart; +import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarDataSet; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.formatter.IndexAxisValueFormatter; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.navigation.NavigationView; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +/** + * Activity for the Data Hub Dashboard. + * Aggregates local CSV scouting data to display team trends, averages, and a leaderboard. + */ public class activityDataShowing extends AppCompatActivity { + private final List allMatches = new ArrayList<>(); + private final Set teamNumbers = new HashSet<>(); + private final Set competitions = new HashSet<>(); + + private MatchHistoryAdapter adapter; + private TextView avgAutoText, avgTeleopText, avgPassingText, climbRateText; + private LineChart scoringChart; + private HorizontalBarChart leaderboardChart; + private Spinner compFilterSpinner, metricSpinner; + private TextView leaderboardPageText; + + private int leaderboardPage = 0; + private static final int PAGE_SIZE = 5; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EdgeToEdge.enable(this); setContentView(R.layout.activity_data_showing); - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + + // UI Initialization + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + DrawerLayout drawer = findViewById(R.id.drawer_layout); + NavigationView navigationView = findViewById(R.id.nav_view); + + // Fix status bar overlap for the AppBar and the Navigation Drawer + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.app_bar), (v, insets) -> { Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + v.setPadding(0, systemBars.top, 0, 0); return insets; }); - final String BASE = "match_scouting_2025"; - String base = BASE + ".csv"; - String comp = BASE + "_comp.csv"; - String radar = BASE + "_radar.csv"; - String aggregated = BASE + "_agg.csv"; - String delimiter = ","; - - ArrayList aggArr = new ArrayList<>(); - - - boolean baseE = false; - boolean compE = false; - boolean radarE = false; - boolean aggE = false; - - try{ - File baseF = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), base); - File compF = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), comp); - File radarF = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), radar); - File aggF = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), aggregated); - - if(baseF.exists()){baseE = true;} - if(compF.exists()){compE = true;} - if(radarF.exists()){radarE = true;} - if(aggF.exists()){aggE = true;} - - if(aggE) { - BufferedReader brAgg = new BufferedReader(new FileReader(aggF)); - String line; - while((line = brAgg.readLine()) != null){ - aggArr.add(line.split(delimiter)); + // Setup Sidebar Toggle + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( + this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); + drawer.addDrawerListener(toggle); + toggle.syncState(); + + // Handle Sidebar Navigation + navigationView.setNavigationItemSelectedListener(item -> { + int id = item.getItemId(); + if (id == R.id.nav_scout) { + Intent intent = new Intent(this, ActivityCompetitionSelection.class); + intent.putExtra("chooseNewCompetition", true); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } + drawer.closeDrawer(GravityCompat.START); + return true; + }); + + // Bind UI Components + avgAutoText = findViewById(R.id.avg_auto_scored); + avgTeleopText = findViewById(R.id.avg_teleop_scored); + avgPassingText = findViewById(R.id.avg_passing); + climbRateText = findViewById(R.id.climb_success_rate); + scoringChart = findViewById(R.id.scoring_line_chart); + leaderboardChart = findViewById(R.id.leaderboard_bar_chart); + compFilterSpinner = findViewById(R.id.comp_filter_spinner); + metricSpinner = findViewById(R.id.leaderboard_metric_spinner); + leaderboardPageText = findViewById(R.id.leaderboard_page_text); + + // Leaderboard Pagination + Button prevButton = findViewById(R.id.prev_leaderboard_button); + Button nextButton = findViewById(R.id.next_leaderboard_button); + prevButton.setOnClickListener(v -> { + if (leaderboardPage > 0) { + leaderboardPage--; + refreshLeaderboard(); + } + }); + nextButton.setOnClickListener(v -> { + leaderboardPage++; + refreshLeaderboard(); + }); + + // Data Lifecycle: Load -> Setup UI -> Filter + setupCharts(); + loadLocalData(); + setupDashboard(); + setupCompSpinner(); + setupMetricSpinner(); + } + + private void setupCharts() { + // Configure visual styles for MPAndroidChart components + scoringChart.getDescription().setEnabled(false); + scoringChart.setDrawGridBackground(false); + scoringChart.getAxisRight().setEnabled(false); + scoringChart.getXAxis().setPosition(XAxis.XAxisPosition.BOTTOM); + scoringChart.getLegend().setEnabled(false); + scoringChart.setNoDataText("Select a team to view scoring trend"); + + leaderboardChart.getDescription().setEnabled(false); + leaderboardChart.setDrawGridBackground(false); + leaderboardChart.getAxisRight().setEnabled(false); + leaderboardChart.getAxisLeft().setAxisMinimum(0f); + + XAxis leaderX = leaderboardChart.getXAxis(); + leaderX.setPosition(XAxis.XAxisPosition.BOTTOM); + leaderX.setDrawGridLines(false); + leaderX.setGranularity(1f); + leaderX.setLabelCount(5); + + leaderboardChart.setExtraOffsets(0, 0, 40, 0); + leaderboardChart.getLegend().setEnabled(false); + leaderboardChart.setFitBars(true); + } + + /** + * Scans the public Documents directory for match_scouting_*.csv files. + * Parses each file into a MatchData object for aggregation. + */ + private void loadLocalData() { + File docDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); + File[] files = docDir.listFiles((dir, name) -> name.startsWith("match_scouting_") && name.endsWith(".csv")); + + if (files == null) return; + + for (File file : files) { + try (BufferedReader br = new BufferedReader(new FileReader(file))) { + String line = br.readLine(); + if (line != null) { + // split with -1 to preserve empty trailing fields (like comments) + String[] data = line.split(",", -1); + MatchData match = new MatchData(data); + if (match.teamNum != null && !match.teamNum.isEmpty()) { + allMatches.add(match); + teamNumbers.add(match.teamNum); + if (match.competition != null && !match.competition.isEmpty()) { + competitions.add(match.competition); + } + } } + } catch (Exception e) { + Log.e("DataShowing", "Error loading file: " + file.getName(), e); } - } catch (Exception e) { - Log.e("e", "onCreate: ", e); } + } + + private void setupDashboard() { + // Setup Search/Autocomplete for team selection + AutoCompleteTextView searchView = findViewById(R.id.team_search_view); + ArrayAdapter teamAdapter = new ArrayAdapter<>(this, + android.R.layout.simple_dropdown_item_1line, new ArrayList<>(teamNumbers)); + searchView.setAdapter(teamAdapter); + + // Setup Match History List + RecyclerView recyclerView = findViewById(R.id.match_history_recycler); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + adapter = new MatchHistoryAdapter(new ArrayList<>(), this::showMatchDetails); + recyclerView.setAdapter(adapter); + + searchView.setOnItemClickListener((parent, view, position, id) -> { + String selectedTeam = (String) parent.getItemAtPosition(position); + updateDashboard(selectedTeam); + }); + } + + /** + * Displays a detailed breakdown of a single match in an AlertDialog. + */ + private void showMatchDetails(MatchData match) { + StringBuilder details = new StringBuilder(); + details.append("Scouter: ").append(match.scout).append("\n"); + details.append("Competition: ").append(match.competition).append("\n"); + details.append("Team Color: ").append(match.teamColor).append("\n\n"); + + details.append("--- Autonomous ---\n"); + details.append("Fuel Scored: ").append(match.autoScored).append("\n"); + details.append("Fuel Passed: ").append(match.autoPassed).append("\n"); + details.append("Accuracy: ").append(match.autoAccuracy).append("\n"); + details.append("Auto Climb (L1): ").append(match.autoHang ? "Yes (+15 pts)" : "No").append("\n\n"); + + details.append("--- TeleOp ---\n"); + details.append("Fuel Scored: ").append(match.teleopScored).append("\n"); + details.append("Fuel Passed: ").append(match.teleopPassed).append("\n"); + details.append("Accuracy: ").append(match.teleopAccuracy).append("\n"); + details.append("Climb Level: ").append(match.hangStatus).append(" (+").append(match.getClimbPoints()).append(" pts)\n"); + details.append("Climb Time: ").append(match.hangTime).append("s\n\n"); + + details.append("--- Performance & Notes ---\n"); + details.append("Accuracy Pos: ").append(match.accuracyPos).append("\n"); + details.append("Played Defense: ").append(match.playedDefense ? "Yes" : "No").append("\n"); + details.append("Defense Notes: ").append(match.defense).append("\n"); + details.append("Stop Reason: ").append(match.stopReason).append("\n"); + details.append("Notes: ").append(match.comments); + + new MaterialAlertDialogBuilder(this) + .setTitle("Match #" + match.matchNum + " Details") + .setMessage(details.toString()) + .setPositiveButton("Close", null) + .show(); + } + + private void setupCompSpinner() { + List compList = new ArrayList<>(); + compList.add("All Competitions"); + compList.addAll(competitions); + + ArrayAdapter spinnerAdapter = new ArrayAdapter<>(this, + android.R.layout.simple_spinner_item, compList); + spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + compFilterSpinner.setAdapter(spinnerAdapter); + + compFilterSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + leaderboardPage = 0; + refreshLeaderboard(); + } + + @Override + public void onNothingSelected(AdapterView parent) {} + }); + } + + private void setupMetricSpinner() { + String[] metrics = {"Total Score", "Auto Fuel", "Tele Fuel", "Passing", "Accuracy %"}; + ArrayAdapter metricAdapter = new ArrayAdapter<>(this, + android.R.layout.simple_spinner_item, metrics); + metricAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + metricSpinner.setAdapter(metricAdapter); + + metricSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + leaderboardPage = 0; + refreshLeaderboard(); + } + + @Override + public void onNothingSelected(AdapterView parent) {} + }); + } + private void refreshLeaderboard() { + String comp = (String) compFilterSpinner.getSelectedItem(); + String metric = (String) metricSpinner.getSelectedItem(); + displayTopTeams(comp.equals("All Competitions") ? null : comp, metric); + } + + /** + * Calculates and displays the top 5 teams based on the selected metric and competition filter. + */ + private void displayTopTeams(String competitionFilter, String metric) { + Map> teamValues = new HashMap<>(); + for (MatchData m : allMatches) { + if (competitionFilter == null || m.competition.equals(competitionFilter)) { + double val = 0; + switch (metric) { + case "Total Score": val = m.getTotalScore(); break; + case "Auto Fuel": val = m.autoScored; break; + case "Tele Fuel": val = m.teleopScored; break; + case "Passing": val = (m.autoPassed + m.teleopPassed); break; + case "Accuracy %": + // Simplified accuracy metric for leaderboard + val = m.getCalculatedTeleopScoredWhole() / Math.max(1.0, m.teleopScored) * 100; + break; + } + teamValues.computeIfAbsent(m.teamNum, k -> new ArrayList<>()).add(val); + } + } + + // Aggregate by Team Number and Sort Descending + List> sortedValues = teamValues.entrySet().stream() + .map(entry -> { + double avg = entry.getValue().stream().mapToDouble(d -> d).average().orElse(0.0); + return new HashMap.SimpleEntry<>(entry.getKey(), avg); + }) + .sorted((e1, e2) -> Double.compare(e2.getValue(), e1.getValue())) + .collect(Collectors.toList()); + + // Handle Pagination + int start = leaderboardPage * PAGE_SIZE; + if (start >= sortedValues.size()) { + if (leaderboardPage > 0) { + leaderboardPage--; + return; + } + leaderboardChart.clear(); + leaderboardChart.setNoDataText("No data for selection"); + leaderboardPageText.setText("No Data"); + return; + } + + int end = Math.min(start + PAGE_SIZE, sortedValues.size()); + List> pageValues = sortedValues.subList(start, end); + + leaderboardPageText.setText(String.format(Locale.getDefault(), "Rank %d-%d", start + 1, end)); + updateLeaderboardChart(pageValues, metric, start); + } + + private void updateLeaderboardChart(List> data, String metricLabel, int startRank) { + List entries = new ArrayList<>(); + List teamLabels = new ArrayList<>(); + + // Reverse for Horizontal Chart display (highest at top) + List> displayData = new ArrayList<>(data); + Collections.reverse(displayData); + + for (int i = 0; i < displayData.size(); i++) { + Map.Entry entry = displayData.get(i); + entries.add(new BarEntry(i, entry.getValue().floatValue())); + int rank = startRank + displayData.size() - i; + teamLabels.add("#" + rank + " Team " + entry.getKey()); + } + + BarDataSet dataSet = new BarDataSet(entries, metricLabel); + dataSet.setColor(ContextCompat.getColor(this, R.color.aztech_blue)); + dataSet.setValueTextSize(10f); + + BarData barData = new BarData(dataSet); + leaderboardChart.setData(barData); + leaderboardChart.getXAxis().setValueFormatter(new IndexAxisValueFormatter(teamLabels)); + leaderboardChart.animateY(800); + leaderboardChart.invalidate(); + } + + /** + * Filters all data for a specific team and updates the summary metrics and trend chart. + */ + private void updateDashboard(String teamNum) { + List teamMatches = new ArrayList<>(); + double totalAuto = 0; + double totalTeleop = 0; + double totalPassing = 0; + int climbs = 0; + + for (MatchData m : allMatches) { + if (m.teamNum.equals(teamNum)) { + teamMatches.add(m); + } + } + + // Sort matches chronologically by match number + teamMatches.sort((m1, m2) -> { + try { + return Integer.compare(Integer.parseInt(m1.matchNum), Integer.parseInt(m2.matchNum)); + } catch (Exception e) { + return m1.matchNum.compareTo(m2.matchNum); + } + }); + + for (MatchData m : teamMatches) { + totalAuto += m.getCalculatedAutoScoredWhole(); + totalTeleop += m.getCalculatedTeleopScoredWhole(); + totalPassing += (m.autoPassed + m.teleopPassed); + if (m.hangStatus != null && !m.hangStatus.equalsIgnoreCase("None") && !m.hangStatus.equalsIgnoreCase("Nothing")) { + climbs++; + } + } + + if (teamMatches.isEmpty()) return; + + double avgAuto = totalAuto / teamMatches.size(); + double avgTele = totalTeleop / teamMatches.size(); + double avgPassing = totalPassing / teamMatches.size(); + double climbRate = (double) climbs / teamMatches.size() * 100; + + // Update Summary Cards + avgAutoText.setText(String.format(Locale.getDefault(), "%.1f", avgAuto)); + avgTeleopText.setText(String.format(Locale.getDefault(), "%.1f", avgTele)); + avgPassingText.setText(String.format(Locale.getDefault(), "%.1f", avgPassing)); + climbRateText.setText(String.format(Locale.getDefault(), "%.0f%%", climbRate)); + + updateTrendChart(teamMatches); + adapter.updateData(teamMatches); + } + + /** + * Updates the scoring trend line chart with the robot's match-by-match total score. + */ + private void updateTrendChart(List teamMatches) { + List entries = new ArrayList<>(); + List labels = new ArrayList<>(); + + for (int i = 0; i < teamMatches.size(); i++) { + MatchData m = teamMatches.get(i); + entries.add(new Entry(i, (float) m.getTotalScore())); + labels.add("M" + m.matchNum); + } + + LineDataSet dataSet = new LineDataSet(entries, "Total Score"); + dataSet.setColor(ContextCompat.getColor(this, R.color.aztech_blue)); + dataSet.setCircleColor(ContextCompat.getColor(this, R.color.aztech_yellow)); + dataSet.setLineWidth(3f); + dataSet.setCircleRadius(5f); + dataSet.setDrawCircleHole(true); + dataSet.setValueTextSize(10f); + dataSet.setDrawFilled(true); + dataSet.setFillColor(ContextCompat.getColor(this, R.color.aztech_blue_accent)); + dataSet.setMode(LineDataSet.Mode.CUBIC_BEZIER); + + LineData lineData = new LineData(dataSet); + scoringChart.setData(lineData); + + scoringChart.getXAxis().setValueFormatter(new IndexAxisValueFormatter(labels)); + scoringChart.animateX(800); + scoringChart.invalidate(); + } + + @Override + public void onBackPressed() { + DrawerLayout drawer = findViewById(R.id.drawer_layout); + if (drawer != null && drawer.isDrawerOpen(GravityCompat.START)) { + drawer.closeDrawer(GravityCompat.START); + } else { + super.onBackPressed(); + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/roboticsscoutingmatchapp/activityPreMatch.java b/app/src/main/java/com/example/roboticsscoutingmatchapp/activityPreMatch.java index c54e3b5..a3881d4 100644 --- a/app/src/main/java/com/example/roboticsscoutingmatchapp/activityPreMatch.java +++ b/app/src/main/java/com/example/roboticsscoutingmatchapp/activityPreMatch.java @@ -10,22 +10,28 @@ import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; import androidx.core.graphics.Insets; +import androidx.core.view.GravityCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import com.google.android.material.navigation.NavigationView; + +/** + * Activity for collecting Pre-Match data (Scouter name, match #, team info). + * Updated for the 2026 REBUILT game pieces (Fuel). + */ public class activityPreMatch extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { - // Defines an object for the utility file because of weird compat with static methods U u = new U(); - /* - Checks for if there is any data sent over with the intent when switching to current - activity, save strings will be compiled and saved as csv in final activity page - */ + // Retrieve existing data strings passed through Intent extras for state persistence String preMatchSaveString, autoSaveString, teleOpSaveString, postMatchSaveString, competitionString, scoutNameString; Bundle extras = getIntent().getExtras(); @@ -45,28 +51,59 @@ protected void onCreate(Bundle savedInstanceState) { scoutNameString = ""; } - // Checks for insets changing (screen rotation) -- Auto-generated super.onCreate(savedInstanceState); EdgeToEdge.enable(this); setContentView(R.layout.activity_pre_match); - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + + // Sidebar/Navigation Setup + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + DrawerLayout drawer = findViewById(R.id.drawer_layout); + NavigationView navigationView = findViewById(R.id.nav_view); + + // Fix status bar overlap for the AppBar and the Navigation Drawer + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.app_bar), (v, insets) -> { Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + v.setPadding(0, systemBars.top, 0, 0); return insets; }); - // Defining all the relevant components in the activity + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( + this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); + drawer.addDrawerListener(toggle); + toggle.syncState(); + + navigationView.setNavigationItemSelectedListener(item -> { + int id = item.getItemId(); + if (id == R.id.nav_scout) { + Intent intent = new Intent(this, ActivityCompetitionSelection.class); + intent.putExtra("chooseNewCompetition", true); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } else if (id == R.id.nav_dashboard) { + startActivity(new Intent(this, activityDataShowing.class)); + } + drawer.closeDrawer(GravityCompat.START); + return true; + }); + + // UI Component Binding EditText scoutName = findViewById(R.id.scout_name); EditText matchNumber = findViewById(R.id.match_number); EditText teamNumber = findViewById(R.id.team_number); RadioGroup teamColorRadioGroup = findViewById(R.id.team_color_radio_group); + + // REBUILT 2026 Fix: Resource ID changed from checkBox_preloaded_coral to fuel CheckBox preloadedFuel = findViewById(R.id.checkBox_preloaded_fuel); + Button saveButton = findViewById(R.id.save_button); Button backButton = findViewById(R.id.back_button); + if(!scoutNameString.isEmpty()){ scoutName.setText(scoutNameString); } + // Parse backward-passed data strings to restore UI state if(!preMatchSaveString.isEmpty()){ competitionString = u.untilNextComma(preMatchSaveString); preMatchSaveString = u.nextCommaOn(preMatchSaveString); // remove competition @@ -88,16 +125,14 @@ protected void onCreate(Bundle savedInstanceState) { } - // Defines a toast (pop-up) to be used when a field is left unfilled Toast unfilledMessage = new Toast(this); unfilledMessage.setDuration(Toast.LENGTH_SHORT); String finalCompetitionString = competitionString; saveButton.setOnClickListener((l) -> { - // Check if all fields are full -// findViewById(R.id.scroll_view); String response = ""; + // Validation logic if(u.getData(scoutName).isEmpty()){ response = getResources().getString(R.string.prompt_scout_name) + " " + getResources().getString(R.string.is_empty_identifier); }else if(u.getData(matchNumber).isEmpty()){ @@ -107,10 +142,10 @@ protected void onCreate(Bundle savedInstanceState) { }else if(u.getData(teamColorRadioGroup).isEmpty()){ response = "Please choose a team color"; }else{ - // Utilizes "savestrings" + // Serialize pre-match info into a comma-separated string for passing between activities Intent i = new Intent(this, activityAutonomous.class); String preMatchInfo = ""; - preMatchInfo += finalCompetitionString + ","; //TODO: Add competition + preMatchInfo += finalCompetitionString + ","; preMatchInfo += u.DATA_VERSION + ","; preMatchInfo += u.stripText(u.getData(scoutName), u.DELIMITER_AND_WHITESPACE) + ","; preMatchInfo += u.stripText(u.getData(teamNumber)) + ","; @@ -139,4 +174,14 @@ protected void onCreate(Bundle savedInstanceState) { }); } -} \ No newline at end of file + + @Override + public void onBackPressed() { + DrawerLayout drawer = findViewById(R.id.drawer_layout); + if (drawer != null && drawer.isDrawerOpen(GravityCompat.START)) { + drawer.closeDrawer(GravityCompat.START); + } else { + super.onBackPressed(); + } + } +} diff --git a/app/src/main/java/com/example/roboticsscoutingmatchapp/activityTeleOp.java b/app/src/main/java/com/example/roboticsscoutingmatchapp/activityTeleOp.java index 58383ab..a91c7be 100644 --- a/app/src/main/java/com/example/roboticsscoutingmatchapp/activityTeleOp.java +++ b/app/src/main/java/com/example/roboticsscoutingmatchapp/activityTeleOp.java @@ -14,6 +14,10 @@ import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +/** + * Activity for collecting TeleOperated period data. + * Tracks fuel scoring, accuracy position, and endgame climb status. + */ public class activityTeleOp extends AppCompatActivity { @Override @@ -29,7 +33,8 @@ protected void onCreate(Bundle savedInstanceState) { U u = new U(); - String preMatchSaveString, autoSaveString, // Gets all savestrings from wherever coming in from + // Load intent state + String preMatchSaveString, autoSaveString, teleOpSaveString, postMatchSaveString; Bundle extras = getIntent().getExtras(); if(extras != null){ @@ -44,12 +49,8 @@ protected void onCreate(Bundle savedInstanceState) { postMatchSaveString = ""; } - // Defining all the access-necessary components within the page - Button backButton = findViewById(R.id.back_button); - Button saveButton = findViewById(R.id.save_button); - + // UI Binding - Scoring & Accuracy EditText FSField = findViewById(R.id.edit_text_fs); - Button FS1plus = findViewById(R.id.up_count_button_fs1); Button FS5plus = findViewById(R.id.up_count_button_fs5); Button FS10plus = findViewById(R.id.up_count_button_fs10); @@ -69,8 +70,8 @@ protected void onCreate(Bundle savedInstanceState) { RadioButton seventyFivePercent = findViewById(R.id.seventyFivePercent); RadioButton overNinetyFive = findViewById(R.id.overNinetyFive); + // UI Binding - Passing & Endgame EditText FPField = findViewById(R.id.edit_text_fp); - Button FP1plus = findViewById(R.id.up_count_button_fp1); Button FP5plus = findViewById(R.id.up_count_button_fp5); Button FP10plus = findViewById(R.id.up_count_button_fp10); @@ -87,6 +88,7 @@ protected void onCreate(Bundle savedInstanceState) { RadioButton level1Button = findViewById(R.id.level1_climb); RadioButton level2Button = findViewById(R.id.level2_climb); RadioButton level3Button = findViewById(R.id.level3_climb); + RadioButton noneButton = findViewById(R.id.nothing); RadioGroup accuracyRadioGroup = findViewById(R.id.accuracy_position); RadioButton pacManButton = findViewById(R.id.pac_manning); @@ -94,7 +96,6 @@ protected void onCreate(Bundle savedInstanceState) { RadioButton noDifferenceButton = findViewById(R.id.no_difference); RadioButton badAccuracyButton = findViewById(R.id.bad_accuracy); - RadioButton noneButton = findViewById(R.id.nothing); RadioGroup endgameTimeGroup = findViewById(R.id.endgame_time); RadioButton twentyFiveButton = findViewById(R.id.twenty_five); RadioButton twentyButton = findViewById(R.id.twenty); @@ -103,12 +104,14 @@ protected void onCreate(Bundle savedInstanceState) { RadioButton fiveButton = findViewById(R.id.five); RadioButton zeroButton = findViewById(R.id.zero); + Button backButton = findViewById(R.id.back_button); + Button saveButton = findViewById(R.id.save_button); + Toast unfilledMessage = new Toast(this); unfilledMessage.setDuration(Toast.LENGTH_SHORT); - // Setting all fields which have data + // Parse saved teleop data to restore UI state if(!teleOpSaveString.isEmpty()){ - // #Fuel Shot | Accuracy % | #Fuel Passed | Hang Status | Hang Time | Accuracy Position | FSField.setText(u.untilNextComma(teleOpSaveString)); teleOpSaveString = u.nextCommaOn(teleOpSaveString); FPField.setText(u.untilNextComma(teleOpSaveString)); @@ -116,86 +119,45 @@ protected void onCreate(Bundle savedInstanceState) { String currentButton = u.untilNextComma(teleOpSaveString); switch(currentButton){ - case "Level 1": - level1Button.toggle(); - break; - case "Level 2": - level2Button.toggle(); - break; - case "Level 3": - level3Button.toggle(); - break; - case "None": - noneButton.toggle(); - break; + case "Level 1": level1Button.toggle(); break; + case "Level 2": level2Button.toggle(); break; + case "Level 3": level3Button.toggle(); break; + case "None": noneButton.toggle(); break; } teleOpSaveString = u.nextCommaOn(teleOpSaveString); String timeToHang = u.untilNextComma(teleOpSaveString); switch(timeToHang){ - case "25": - twentyFiveButton.toggle(); - break; - case "20": - twentyButton.toggle(); - break; - case "15": - fifteenButton.toggle(); - break; - case "10": - tenButton.toggle(); - break; - case "5": - fiveButton.toggle(); - break; - case "0": - zeroButton.toggle(); - break; + case "25": twentyFiveButton.toggle(); break; + case "20": twentyButton.toggle(); break; + case "15": fifteenButton.toggle(); break; + case "10": tenButton.toggle(); break; + case "5": fiveButton.toggle(); break; + case "0": zeroButton.toggle(); break; } teleOpSaveString = u.nextCommaOn(teleOpSaveString); - String accuracyPosition = u.untilNextComma(teleOpSaveString); - switch(accuracyPosition){ - case "When moving": - pacManButton.toggle(); - break; - case "When unmoving": - standStillButton.toggle(); - break; - case "About the same": - noDifferenceButton.toggle(); - break; - case "Consistently bad accuracy": - badAccuracyButton.toggle(); - break; + String accuracyPos = u.untilNextComma(teleOpSaveString); + switch(accuracyPos){ + case "When moving": pacManButton.toggle(); break; + case "When unmoving": standStillButton.toggle(); break; + case "About the same": noDifferenceButton.toggle(); break; + case "Consistently bad accuracy": badAccuracyButton.toggle(); break; } teleOpSaveString = u.nextCommaOn(teleOpSaveString); String accuracyChoice = u.untilNextComma(teleOpSaveString); switch(accuracyChoice){ - case "Less Than 10%": - lessThanTen.toggle(); - break; - case "25%": - twentyFivePercent.toggle(); - break; - case "50%": - fiftyPercent.toggle(); - break; - case "75%": - seventyFivePercent.toggle(); - break; - case "More Than 95%": - overNinetyFive.toggle(); - break; + case "Less Than 10%": lessThanTen.toggle(); break; + case "25%": twentyFivePercent.toggle(); break; + case "50%": fiftyPercent.toggle(); break; + case "75%": seventyFivePercent.toggle(); break; + case "More Than 95%": overNinetyFive.toggle(); break; } - - teleOpSaveString = u.nextCommaOn(teleOpSaveString); - } - // Setting increment and decrement listeners for all buttons + // Increment/Decrement Listeners for TeleOp Fuel FS1plus.setOnClickListener((l)->u.incrementText(FSField)); FS1minus.setOnClickListener((l)->u.incrementText(FSField, -1)); FS5plus.setOnClickListener((l)->u.incrementText(FSField, +5)); @@ -218,17 +180,11 @@ protected void onCreate(Bundle savedInstanceState) { FP20plus.setOnClickListener((l)->u.incrementText(FPField, +20)); FP20minus.setOnClickListener((l)->u.incrementText(FPField, -20)); - - // Back button, which sends data backwards even if it's unfilled backButton.setOnClickListener((l)->{ String teleOpInfo = ""; - // #Fuel Shot | Accuracy % | #Fuel Passed | Hang Status | Hang Time | Accuracy Position | - teleOpInfo += u.getData(FSField) + ","; teleOpInfo += u.getData(accuracyGroup) + ","; - teleOpInfo += u.getData(FPField) + ","; - teleOpInfo += u.getData(parkRadioGroup) + ","; teleOpInfo += u.getData(endgameTimeGroup) + ","; teleOpInfo += u.getData(accuracyRadioGroup) + ","; @@ -238,32 +194,27 @@ protected void onCreate(Bundle savedInstanceState) { i.putExtra("auto", autoSaveString); i.putExtra("teleOp", teleOpInfo); i.putExtra("postMatch", postMatchSaveString); - this.startActivity(i); }); saveButton.setOnClickListener((l) -> { String response = ""; - if (u.getData(FSField).isEmpty()) - FSField.setText("0"); - if(u.getData(FPField).isEmpty()) - FPField.setText("0"); + if (u.getData(FSField).isEmpty()) FSField.setText("0"); + if(u.getData(FPField).isEmpty()) FPField.setText("0"); + if ((u.getData(accuracyGroup).isEmpty())) response = "Please give accuracy"; - if ((u.getData(accuracyRadioGroup).isEmpty())) + else if ((u.getData(accuracyRadioGroup).isEmpty())) response = "Please give accuracy position"; - if(u.getData(parkRadioGroup).isEmpty()) + else if(u.getData(parkRadioGroup).isEmpty()) response = "Please select an endgame position"; else if(u.getData(endgameTimeGroup).isEmpty()) response = "Please select park time"; else{ String teleOpInfo = ""; - teleOpInfo += u.getData(FSField) + ","; teleOpInfo += u.getData(accuracyGroup) + ","; - teleOpInfo += u.getData(FPField) + ","; - teleOpInfo += u.getData(parkRadioGroup) + ","; teleOpInfo += u.getData(endgameTimeGroup) + ","; teleOpInfo += u.getData(accuracyRadioGroup) + ","; @@ -273,7 +224,6 @@ else if(u.getData(endgameTimeGroup).isEmpty()) i.putExtra("auto", autoSaveString); i.putExtra("teleOp", teleOpInfo); i.putExtra("postMatch", postMatchSaveString); - this.startActivity(i); } if(!response.isEmpty()){ @@ -282,4 +232,4 @@ else if(u.getData(endgameTimeGroup).isEmpty()) } }); } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/activity_after_match.xml b/app/src/main/res/layout/activity_after_match.xml index 5bcba50..4d3d9e1 100644 --- a/app/src/main/res/layout/activity_after_match.xml +++ b/app/src/main/res/layout/activity_after_match.xml @@ -326,4 +326,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_competition_selection.xml b/app/src/main/res/layout/activity_competition_selection.xml index 21d55a9..efdb21b 100644 --- a/app/src/main/res/layout/activity_competition_selection.xml +++ b/app/src/main/res/layout/activity_competition_selection.xml @@ -1,165 +1,113 @@ - - + + - + + - + + + + + + + - - - - - - - - - - - - - - - - - - - + + - - -