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