commit b35f866ee75d3485c6987372badcf4e67233a8c8 Author: melancholytron Date: Fri Jan 3 19:05:33 2025 -0600 First Commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..32f95c0 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +hi \ No newline at end of file diff --git a/fatwalk.py b/fatwalk.py new file mode 100644 index 0000000..03675b5 --- /dev/null +++ b/fatwalk.py @@ -0,0 +1,468 @@ +import sys +from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QLabel, + QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout, + QGridLayout, QFrame, QSlider, QRadioButton, + QButtonGroup, QCheckBox, QComboBox, QStackedWidget) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont, QDoubleValidator + +class VO2Calculator(QFrame): + def __init__(self, parent=None): + super().__init__(parent) + self.setFrameStyle(QFrame.StyledPanel) + self.layout = QVBoxLayout(self) + + # Initialize inputs dictionary + self.inputs = {} # Add this line at the beginning + + # Create method selector + self.method_selector = QComboBox() + self.method_selector.addItems([ + "Age/RHR Method", + "Heart Rate Reserve Method", + "Cooper Test (12-min run)", + "Rockport Walking Test", + "Lab Test Results", + ]) + + self.layout.addWidget(QLabel("VO2 Max Calculation Method:")) + self.layout.addWidget(self.method_selector) + + # Create stacked widget for different input methods + self.stacked_widget = QStackedWidget() + + # Create different input panels + self.age_rhr_panel = self.create_age_rhr_panel() + self.hrr_panel = self.create_hrr_panel() + self.cooper_panel = self.create_cooper_panel() + self.rockport_panel = self.create_rockport_panel() + self.lab_panel = self.create_lab_panel() + + # Add panels to stacked widget + self.stacked_widget.addWidget(self.age_rhr_panel) + self.stacked_widget.addWidget(self.hrr_panel) + self.stacked_widget.addWidget(self.cooper_panel) + self.stacked_widget.addWidget(self.rockport_panel) + self.stacked_widget.addWidget(self.lab_panel) + + self.layout.addWidget(self.stacked_widget) + + # Connect method selector to panel switching + self.method_selector.currentIndexChanged.connect(self.stacked_widget.setCurrentIndex) + + def create_age_rhr_panel(self): + panel = QWidget() + layout = QGridLayout(panel) + + self.inputs['resting_hr'] = QLineEdit() + layout.addWidget(QLabel("Resting Heart Rate (bpm):"), 0, 0) + layout.addWidget(self.inputs['resting_hr'], 0, 1) + + return panel + + def create_hrr_panel(self): + panel = QWidget() + layout = QGridLayout(panel) + + self.inputs['max_hr'] = QLineEdit() + self.inputs['exercise_hr'] = QLineEdit() + self.inputs['exercise_intensity'] = QComboBox() + self.inputs['exercise_intensity'].addItems([ + 'Light (50%)', 'Moderate (65%)', + 'Hard (80%)', 'Very Hard (90%)' + ]) + + layout.addWidget(QLabel("Maximum Heart Rate (bpm):"), 0, 0) + layout.addWidget(self.inputs['max_hr'], 0, 1) + layout.addWidget(QLabel("Exercise Heart Rate (bpm):"), 1, 0) + layout.addWidget(self.inputs['exercise_hr'], 1, 1) + layout.addWidget(QLabel("Exercise Intensity:"), 2, 0) + layout.addWidget(self.inputs['exercise_intensity'], 2, 1) + + return panel + + def create_cooper_panel(self): + panel = QWidget() + layout = QGridLayout(panel) + + self.inputs['cooper_distance'] = QLineEdit() + layout.addWidget(QLabel("Distance covered in 12 minutes (meters):"), 0, 0) + layout.addWidget(self.inputs['cooper_distance'], 0, 1) + + return panel + + def create_rockport_panel(self): + panel = QWidget() + layout = QGridLayout(panel) + + self.inputs['walk_time'] = QLineEdit() + self.inputs['end_hr'] = QLineEdit() + + layout.addWidget(QLabel("Time to walk 1 mile (minutes):"), 0, 0) + layout.addWidget(self.inputs['walk_time'], 0, 1) + layout.addWidget(QLabel("Heart Rate at end (bpm):"), 1, 0) + layout.addWidget(self.inputs['end_hr'], 1, 1) + + return panel + + def create_lab_panel(self): + panel = QWidget() + layout = QGridLayout(panel) + + self.inputs['lab_vo2'] = QLineEdit() + layout.addWidget(QLabel("Lab Test VO2 Max Result:"), 0, 0) + layout.addWidget(self.inputs['lab_vo2'], 0, 1) + + return panel + + def calculate_vo2max(self, age, gender, weight_lbs): + """Calculate VO2 max based on selected method""" + method = self.method_selector.currentIndex() + + try: + if method == 0: # Age/RHR Method + resting_hr = float(self.inputs['resting_hr'].text()) + # Using the Heart Rate Reserve (HRR) method with estimated max HR + max_hr = 220 - age + hrr = max_hr - resting_hr + + if gender.lower() == 'male': + vo2max = 15.3 * (hrr/resting_hr) + else: + vo2max = 15.3 * (hrr/resting_hr) * 0.9 + + elif method == 1: # Heart Rate Reserve Method + max_hr = float(self.inputs['max_hr'].text()) + exercise_hr = float(self.inputs['exercise_hr'].text()) + resting_hr = float(self.inputs['resting_hr'].text()) + + hrr = max_hr - resting_hr + intensity = ((exercise_hr - resting_hr) / hrr) + vo2max = (exercise_hr / max_hr) * 100 + + elif method == 2: # Cooper Test + distance = float(self.inputs['cooper_distance'].text()) + vo2max = (distance - 504.9) / 44.73 + + elif method == 3: # Rockport Walking Test + time = float(self.inputs['walk_time'].text()) + end_hr = float(self.inputs['end_hr'].text()) + weight_kg = weight_lbs * 0.453592 + + if gender.lower() == 'male': + vo2max = 132.853 - (0.0769 * weight_kg) - (0.3877 * age) + (6.315 * 1) - (3.2649 * time) - (0.1565 * end_hr) + else: + vo2max = 132.853 - (0.0769 * weight_kg) - (0.3877 * age) + (6.315 * 0) - (3.2649 * time) - (0.1565 * end_hr) + + elif method == 4: # Lab Test + vo2max = float(self.inputs['lab_vo2'].text()) + + return round(vo2max, 1) + + except ValueError: + return None + + def get_current_method(self): + return self.method_selector.currentText() +class HikingCalculator(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Hiking Difficulty Calculator") + self.setMinimumWidth(700) + + # Create main widget and layout + main_widget = QWidget() + self.setCentralWidget(main_widget) + layout = QVBoxLayout(main_widget) + + # Create input fields + input_frame = QFrame() + input_frame.setFrameStyle(QFrame.StyledPanel) + input_layout = QGridLayout(input_frame) + + # Create labels and input fields + self.inputs = {} + input_fields = { + 'distance': 'Distance (miles)', + 'elevation': 'Elevation Gain (feet)', + 'height': 'Height (inches)', + 'weight': 'Weight (lbs)', + 'age': 'Age (years)', + 'vo2max': 'VO2 Max' + } + + row = 0 + for key, label_text in input_fields.items(): + label = QLabel(label_text) + input_field = QLineEdit() + input_field.setValidator(QDoubleValidator()) + self.inputs[key] = input_field + input_layout.addWidget(label, row, 0) + input_layout.addWidget(input_field, row, 1) + row += 1 + + # Add gender selection + gender_label = QLabel("Gender:") + self.gender_group = QButtonGroup() + male_radio = QRadioButton("Male") + female_radio = QRadioButton("Female") + male_radio.setChecked(True) + self.gender_group.addButton(male_radio, 1) + self.gender_group.addButton(female_radio, 2) + + gender_layout = QHBoxLayout() + gender_layout.addWidget(male_radio) + gender_layout.addWidget(female_radio) + + input_layout.addWidget(gender_label, row, 0) + input_layout.addLayout(gender_layout, row, 1) + row += 1 + + # Add trail type selection + trail_type_label = QLabel("Trail Type:") + self.trail_type_group = QButtonGroup() + self.one_way_radio = QRadioButton("One Way") + self.loop_radio = QRadioButton("Loop/Round Trip") + self.one_way_radio.setChecked(True) + self.trail_type_group.addButton(self.one_way_radio, 1) + self.trail_type_group.addButton(self.loop_radio, 2) + + trail_type_layout = QHBoxLayout() + trail_type_layout.addWidget(self.one_way_radio) + trail_type_layout.addWidget(self.loop_radio) + + input_layout.addWidget(trail_type_label, row, 0) + input_layout.addLayout(trail_type_layout, row, 1) + row += 1 + + layout.addWidget(input_frame) + + # Add VO2 Calculator + self.vo2_calculator = VO2Calculator() + layout.addWidget(self.vo2_calculator) + + # Add auto-calculate VO2 max checkbox + self.auto_vo2_checkbox = QCheckBox("Auto-calculate VO2 Max") + self.auto_vo2_checkbox.setChecked(True) + self.auto_vo2_checkbox.stateChanged.connect(self.toggle_vo2_input) + layout.addWidget(self.auto_vo2_checkbox) + + # Create terrain slider frame + terrain_frame = QFrame() + terrain_frame.setFrameStyle(QFrame.StyledPanel) + terrain_layout = QVBoxLayout(terrain_frame) + + terrain_label = QLabel("Terrain Difficulty:") + terrain_layout.addWidget(terrain_label) + + self.terrain_slider = QSlider(Qt.Horizontal) + self.terrain_slider.setMinimum(1) + self.terrain_slider.setMaximum(10) + self.terrain_slider.setValue(5) + self.terrain_slider.setTickPosition(QSlider.TicksBelow) + self.terrain_slider.setTickInterval(1) + + self.terrain_description = QLabel() + self.update_terrain_description(5) + self.terrain_slider.valueChanged.connect(self.update_terrain_description) + + terrain_layout.addWidget(self.terrain_slider) + terrain_layout.addWidget(self.terrain_description) + layout.addWidget(terrain_frame) + + # Create calculate button + calc_button = QPushButton("Calculate Difficulty") + calc_button.clicked.connect(self.calculate_difficulty) + calc_button.setStyleSheet(""" + QPushButton { + background-color: #4CAF50; + color: white; + padding: 8px; + font-size: 14px; + border-radius: 4px; + } + QPushButton:hover { + background-color: #45a049; + } + """) + layout.addWidget(calc_button) + + # Create result display + result_frame = QFrame() + result_frame.setFrameStyle(QFrame.StyledPanel) + result_layout = QVBoxLayout(result_frame) + + self.score_label = QLabel("Difficulty Score: ") + self.score_label.setAlignment(Qt.AlignCenter) + self.score_label.setFont(QFont('Arial', 14, QFont.Bold)) + + self.rating_label = QLabel("Rating: ") + self.rating_label.setAlignment(Qt.AlignCenter) + self.rating_label.setFont(QFont('Arial', 12)) + + self.grade_label = QLabel("Grade: ") + self.grade_label.setAlignment(Qt.AlignCenter) + self.grade_label.setFont(QFont('Arial', 12)) + + self.vo2_adjusted_label = QLabel("VO2 Max Adjusted Rating: ") + self.vo2_adjusted_label.setAlignment(Qt.AlignCenter) + self.vo2_adjusted_label.setFont(QFont('Arial', 12)) + + result_layout.addWidget(self.grade_label) + result_layout.addWidget(self.score_label) + result_layout.addWidget(self.rating_label) + result_layout.addWidget(self.vo2_adjusted_label) + layout.addWidget(result_frame) + + # Initially disable VO2 max input + self.inputs['vo2max'].setEnabled(False) + + # Connect age input to auto-update VO2 max + self.inputs['age'].textChanged.connect(self.update_vo2_max) + + def toggle_vo2_input(self, state): + self.inputs['vo2max'].setEnabled(not state) + if state: + self.update_vo2_max() + + def update_terrain_description(self, value): + terrain_descriptions = { + 1: "Paved road or smooth path", + 2: "Well-maintained trail, packed dirt", + 3: "Gravel path with some uneven spots", + 4: "Natural trail with roots and small rocks", + 5: "Mixed terrain with moderate obstacles", + 6: "Rocky trail with frequent obstacles", + 7: "Rough terrain with loose rocks and steep sections", + 8: "Technical terrain with scrambling required", + 9: "Very difficult terrain with constant obstacles", + 10: "Extreme terrain requiring careful navigation" + } + self.terrain_description.setText(f"Level {value}: {terrain_descriptions[value]}") + + def calculate_grade(self, distance_miles, elevation_gain_feet): + # Convert distance to feet + distance_feet = distance_miles * 5280 + # If it's a loop/round trip, calculate grade based on half the distance + if self.loop_radio.isChecked(): + distance_feet = distance_feet / 2 + # Calculate grade as percentage + grade = (elevation_gain_feet / distance_feet) * 100 + return grade + + def calculate_vo2_adjustment(self, vo2max): + # Adjust difficulty based on VO2 max + if vo2max >= 50: + return 0.8 # Very fit + elif vo2max >= 40: + return 0.9 # Above average + elif vo2max >= 30: + return 1.0 # Average + elif vo2max >= 20: + return 1.2 # Below average + else: + return 1.4 # Poor + + def calculate_hike_difficulty(self, distance_miles, elevation_gain_feet, + hiker_height_inches, hiker_weight_lbs, + terrain_difficulty, vo2max): + # Calculate grade + grade = self.calculate_grade(distance_miles, elevation_gain_feet) + + # Base difficulty starts with distance + distance_factor = distance_miles / 5 + + # Elevation gain factor + elevation_factor = elevation_gain_feet / 1000 + + # Grade factor + grade_factor = (grade / 10) ** 1.5 + + # Terrain factor + terrain_factor = terrain_difficulty / 5 + + # BMI calculation + bmi = (hiker_weight_lbs * 703) / (hiker_height_inches ** 2) + + # BMI factor + if bmi < 18.5: + bmi_factor = 1.3 + elif bmi >= 18.5 and bmi < 25: + bmi_factor = 1.0 + elif bmi >= 25 and bmi < 30: + bmi_factor = 1.2 + else: + bmi_factor = 1.4 + + # Calculate base difficulty + raw_score = (distance_factor + elevation_factor + grade_factor) * bmi_factor * terrain_factor + base_score = min(10, max(1, round(raw_score, 1))) + + # Calculate VO2 adjusted score + vo2_adjustment = self.calculate_vo2_adjustment(vo2max) + adjusted_score = min(10, max(1, round(raw_score * vo2_adjustment, 1))) + + return base_score, adjusted_score, grade + + def print_difficulty_rating(self, score): + if score <= 2: + return "Easy" + elif score <= 4: + return "Moderate" + elif score <= 6: + return "Challenging" + elif score <= 8: + return "Difficult" + else: + return "Very Difficult" + + def update_vo2_max(self): + """Calculate and update VO2 max when age, gender, or resting HR changes""" + if self.auto_vo2_checkbox.isChecked(): + try: + age = int(self.inputs['age'].text()) + gender = 'male' if self.gender_group.checkedId() == 1 else 'female' + weight = float(self.inputs['weight'].text()) + + # Check if there's a value in the resting HR field + resting_hr_text = self.vo2_calculator.inputs['resting_hr'].text() + if resting_hr_text: # Only calculate if resting HR is provided + vo2max = self.vo2_calculator.calculate_vo2max(age, gender, weight) + if vo2max is not None: + self.inputs['vo2max'].setText(str(vo2max)) + except ValueError: + pass + + def calculate_difficulty(self): + try: + distance = float(self.inputs['distance'].text()) + elevation = float(self.inputs['elevation'].text()) + height = float(self.inputs['height'].text()) + weight = float(self.inputs['weight'].text()) + vo2max = float(self.inputs['vo2max'].text()) + terrain = self.terrain_slider.value() + + difficulty, adjusted_difficulty, grade = self.calculate_hike_difficulty( + distance, elevation, height, weight, terrain, vo2max) + + base_rating = self.print_difficulty_rating(difficulty) + adjusted_rating = self.print_difficulty_rating(adjusted_difficulty) + + self.grade_label.setText(f"Grade: {grade:.1f}%") + self.score_label.setText(f"Base Difficulty Score: {difficulty}/10") + self.score_label.setStyleSheet(f"color: {'red' if difficulty > 7 else 'green'};") + self.rating_label.setText(f"Base Rating: {base_rating}") + self.vo2_adjusted_label.setText( + f"VO2 Adjusted: {adjusted_difficulty}/10 ({adjusted_rating})") + + except ValueError: + self.score_label.setText("Please fill in all fields with valid numbers") + self.rating_label.setText("") + self.grade_label.setText("") + self.vo2_adjusted_label.setText("") + +if __name__ == '__main__': + app = QApplication(sys.argv) + calculator = HikingCalculator() + calculator.show() + sys.exit(app.exec_()) \ No newline at end of file