You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
468 lines
18 KiB
468 lines
18 KiB
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_())
|