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_())