commit
b35f866ee7
2 changed files with 469 additions and 0 deletions
-
1README.md
-
468fatwalk.py
@ -0,0 +1 @@ |
|||
hi |
|||
@ -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_()) |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue