-
Notifications
You must be signed in to change notification settings - Fork 0
/
gear_functions.py
421 lines (375 loc) · 17.9 KB
/
gear_functions.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
from datetime import datetime, date, timedelta
from config import trails_api_key, weather_api_key, historical_weather_api_key, sun_api_key
import requests
import json
def get_sun_info(latitude, longitude, selected_date):
""" Return sunrise and sunset times for lat/lon for selected date """
url = "https://api.ipgeolocation.io/astronomy"
params = {"apiKey":sun_api_key, "lat":latitude, "long":longitude,
"date":selected_date}
response = requests.get(url = url, params = params)
data_json = response.json()
sunrise = datetime.strptime(data_json["sunrise"], "%H:%M").strftime("%I:%M %p")
sunset = datetime.strptime(data_json["sunset"], "%H:%M").strftime("%I:%M %p")
return sunrise, sunset
def convert_wind_direction(wdir):
""" Convert wind direction in degrees to cardinal direction """
cardinal_directions = ["N", "NNE", "NE", "ENE", "E", "ESE",
"SE", "SSE", "S", "SSW", "SW", "WSW",
"W", "WNW", "NW", "NNW", "N"]
index = int(round(wdir / 22.5, 0))
return cardinal_directions[index]
def get_weather_forecast(latitude, longitude, selected_date):
""" Gets weather data for days <= 14 days from today's date """
# Number of days from today to selected_date
forecast_days = (selected_date - date.today()).days + 1
# Visual Crossing forecast API call
url = "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/weatherdata/forecast"
params = {"locations":f"{latitude},{longitude}", "aggregateHours":"24",
"unitGroup":"us", "shortColumnNames":"true", "contentType":"json",
"key":weather_api_key, "forecastDays":f"{forecast_days}"}
response = requests.get(url = url, params = params)
data_json = response.json()["locations"][f"{latitude},{longitude}"]
# Select forecast data for the selected day
forecast = data_json["values"][forecast_days - 1]
# get sunrise and sunset info from separate API call
sunrise, sunset = get_sun_info(latitude, longitude, selected_date)
# convert from degree wind direction to cardinal direction
wind_direction = convert_wind_direction(forecast["wdir"])
weather_data = {
"today" : date.today(),
"date" : selected_date,
"conditions" : forecast["conditions"] or None,
"temperature" : forecast["temp"],
"max_temp" : forecast["maxt"],
"min_temp" : forecast["mint"],
"wind_speed" : forecast["wspd"],
"wind_direction": wind_direction,
"humidity" : forecast["humidity"],
"prob_of_precip": forecast["pop"],
"precip" : None,
"snow_depth" : forecast["snowdepth"],
"snow_accum" : None,
"cloud_cover" : forecast["cloudcover"],
"sunrise" : sunrise,
"sunset" : sunset
}
return weather_data
def get_historical_weather(latitude, longitude, selected_date):
"""
Get historical weather data if selected date > 14 days from todays' date
"""
selected_date = selected_date - timedelta(days=365)
url = "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/weatherdata/history"
params = {"aggregateHours":"24", "location":f"{latitude},{longitude}",
"unitGroup":"us", "startDateTime":selected_date,
"endDateTime":selected_date, "contentType":"json",
"key":weather_api_key}
response = requests.get(url = url, params = params)
data_json = response.json()["locations"][f"{latitude},{longitude}"]
sunrise, sunset = get_sun_info(latitude, longitude, selected_date)
weather_data = {
"today" : date.today(),
"date" : selected_date,
"conditions" : None,
"temperature" : data_json["values"][0]["temp"],
"max_temp" : data_json["values"][0]["maxt"],
"min_temp" : data_json["values"][0]["mint"],
"wind_speed" : None,
"wind_direction": None,
"humidity" : None,
"prob_of_precip": None,
"precip" : data_json["values"][0]["precip"],
"snow_depth" : None,
"snow_accum" : data_json["values"][0]["snowdepth"],
"cloud_cover" : None,
"sunrise" : sunrise or None,
"sunset" : sunset or None
}
# precipitation accumulation <= 0.1 not considered significant
if weather_data["precip"] <= 0.1:
weather_data["precip"] = 0
return weather_data
def get_weather_data(latitude, longitude, selected_date):
"""
Gets current weather data from weather API based on given latitude/longitude.
Get forecast weather data if selected date <= 14 days from today, otherwise
get historical weather data.
"""
latitude = round(latitude, 2)
longitude = round(longitude, 2)
today = date.today()
forecast_date = today + timedelta(days=14)
if selected_date <= forecast_date:
# Use Visual Crossing API call for current and forecast weather is
# selected date is <= 14 days from today's date
weather_data = get_weather_forecast(latitude, longitude, selected_date)
historical = False
else:
# Use Weather Company API call for historical weather if selected date
# is greater than 14 days from today's date
weather_data = get_historical_weather(latitude, longitude, selected_date)
historical = True
return weather_data, historical
def get_trail_data(trail_id):
"""
Gets hiking trail data based on trail_id from hiking project API.
Add hiking_time and average trail grade key/value to trail data.
"""
url = "https://www.hikingproject.com/data/get-trails-by-id"
params = {"ids":trail_id, "key":trails_api_key}
response = requests.get(url = url, params = params)
if response.status_code == 200:
trail_data = response.json()["trails"][0]
trail_data["hiking_time"] = calculate_hiking_time(trail_data["length"],
trail_data["ascent"])
trail_data["grade"] = calculate_grade(trail_data["length"],
trail_data["ascent"])
else:
trail_data = None
# with open("trail_data.json", "w") as write_file:
# json.dump(data_json, write_file, indent=4)
return trail_data
def gear_evaluation(trail_data, weather_data):
"""
Evaluate trail_data and weather_data and get gear that match criteria.
Pulls in data from gear_data.json file to match with trail and weather data.
Returns a list of attribute dictionaries with attribute description and gear.
"""
temperature_attribute = evaluate_temperature(weather_data["temperature"])
precipitation_attribute = evaluate_precipitation(weather_data)
snow_attribute = evaluate_snow(weather_data)
length_attribute = evaluate_length(trail_data["hiking_time"])
elevation_attribute = evaluate_elevation(trail_data["grade"], trail_data["ascent"])
wind_attribute = evaluate_wind(weather_data["wind_speed"])
# attributes are categories that define the trail and weather conditions
# they are used to match up the trail/weather info with gear
attributes = ["all", temperature_attribute, precipitation_attribute,
snow_attribute, length_attribute, elevation_attribute,
wind_attribute]
gear_meta_data = build_gear_meta_data()
gear_dict = get_gear(attributes, gear_meta_data)
return gear_dict
def evaluate_temperature(temperature):
"""
Evaluates given temperature and returns a temperature category.
"""
if temperature <= 30:
return "freezing_temp"
elif 30 < temperature <= 45:
return "cold_temp"
elif 45 < temperature <= 65:
return "cool_temp"
elif 65 < temperature <= 80:
return "moderate_temp"
elif 80 < temperature <= 90:
return "warm_temp"
else:
# temp is > 90
return "hot_temp"
def evaluate_precipitation(weather_data):
"""
Rain is considered possible when:
1. If forecast weather and propability of precipitation is > 0
2. If historical weather and precipitation accumulation in the past has
been > 0.1
"""
if (weather_data["prob_of_precip"] and weather_data["prob_of_precip"] > 0) or \
(weather_data["precip"] and weather_data["precip"] > 0.1):
return "rain"
else:
return None
def evaluate_snow(weather_data):
"""
Snow is possible when:
1. If forecast weather and there is snow on the ground
2. If historical weather and there is a history of any snow accumulation
"""
if (weather_data["snow_depth"] and weather_data["snow_depth"] > 0) or \
(weather_data["snow_accum"] and weather_data["snow_accum"] > 0):
return "snow"
else:
return None
def evaluate_length(hiking_time):
"""
Evaluates trail length based on distance and elevation.
A hiking time > 3 hours is considered "medium_duration"
A hiking time > 6 hours is considered "long_duration"
"""
if hiking_time > 360:
return "long_duration"
elif hiking_time > 180:
return "medium_duration"
else:
return None
def calculate_hiking_time(length, elevation_change):
"""
Calculate hiking time estimate based on Naismith's rule, uses trail
length in miles, elevation_change in feet, and returns time in hours.
https://en.wikipedia.org/wiki/Naismith%27s_rule
Naismith's rule: Allow one hour for every 3 miles (5 km) forward, plus an
additional hour for every 2,000 feet (600 m) of ascent. The basic rule
assumes hikers of reasonable fitness, on typical terrain, and under normal
conditions.
"""
# time (minutes) = length (in miles) * 20 minutes/mile
# + elevation_change (in feet) * 30 min/1000 feet
hiking_time = length * 20 + elevation_change * 30/1000
# convert time from minutes to hours and round to 1 decimal place
return round((hiking_time / 60), 1)
def evaluate_elevation(grade, ascent):
"""
Determine if a hike is considered steep or if it has an overall high
elevation gain.
"""
if grade > 10 or ascent > 750:
return "steep"
else:
return None
def calculate_grade(length, ascent):
""" Calculate average trail grade """
distance_feet = length * 5280 # 5280 feet/mile
if distance_feet > 0:
grade = ascent / distance_feet # calculate % grade
else:
grade = 0
return round(grade * 100, 1)
def evaluate_wind(wind_speed):
""" Evaluate wind conditions. """
if wind_speed and wind_speed > 19:
return "wind"
"""
GEAR DATA
"""
class Gear_Item:
""" Represents a single gear item and its category. """
def __init__(self, item_name, category):
self._item_name = item_name
self._category = category
def __repr__(self):
return self._item_name
def get_category(self):
return self._category
def get_gear_item(self):
return {self._item_name : {"category" : self._category}}
class Use_Condition:
"""
Represents a use condition, each use condition has a description and
has gear items that fall under that use condition.
"""
def __init__(self, name, description, icon):
self._name = name
self._description = description
self._icon = icon
self._gear = {}
def set_gear(self, gear_list):
for gear_item in gear_list:
self._gear.update(gear_item.get_gear_item())
# self._gear.append(gear_item.get_gear_item())
def get_use_condition(self):
return {self._name : {"description" : self._description,
"gear" : self._gear, "icon" : self._icon}}
def print_use_condition(self):
print(self._name, self._description, self._gear)
def build_gear_meta_data():
""" Build all of the gear and use condition data. """
gloves = Gear_Item("gloves", "clothing")
warm_jacket = Gear_Item("warm, insulated jacket", "clothing")
warm_boots = Gear_Item("warm boots", "footwear")
warm_hat = Gear_Item("warm hat", "clothing")
long_underwear = Gear_Item("long underwear", "clothing")
warm_socks = Gear_Item("warm socks", "footwear")
waterproof_socks = Gear_Item("waterproof socks", "footwear")
daypack = Gear_Item("daypack", "gear")
water = Gear_Item("water", "food and water")
rain_pants = Gear_Item("rain pants", "clothing")
rain_jacket = Gear_Item("rain jacket", "clothing")
snowshoes = Gear_Item("snowshoes", "footwear")
gaiters = Gear_Item("gaiters", "clothing")
hiking_boots = Gear_Item("hiking boots", "footwear")
map_gps = Gear_Item("map/GPS device", "navigation")
light_med_jacket = Gear_Item("light-medium jacket", "clothing")
light_jacket = Gear_Item("light jacket", "clothing")
hiking_shoes = Gear_Item("hiking shoes", "footwear")
moisture_wicking_clothing = Gear_Item("moisture wicking clothing", "clothing")
shorts = Gear_Item("shorts", "clothing")
lunch = Gear_Item("lunch", "food and water")
extra_water = Gear_Item("extra water", "food and water")
extra_food = Gear_Item("extra food", "food and water")
trekking_poles = Gear_Item("trekking poles", "gear")
waterproof_shoes = Gear_Item("waterproof shoes", "footwear")
snacks = Gear_Item("snacks", "food and water")
sun_protecetion = Gear_Item("sun protection", "other")
first_aid = Gear_Item("first aid kit", "other")
hiking_sandals = Gear_Item("hiking sandals", "footwear")
sun_hat = Gear_Item("sun hat", "clothing")
face_mask = Gear_Item("face mask/balaclava", "clothing")
snow_goggles = Gear_Item("snow goggles", "other")
microspikes = Gear_Item("microspikes/crampons", "gear")
windbreaker = Gear_Item("windbreaker", "clothing")
freezing = Use_Condition("freezing_temp", "Temperature < 31 \u00b0F.", "fas fa-thermometer-empty")
freezing.set_gear([warm_jacket, warm_boots, gloves, warm_hat,
long_underwear, warm_socks, face_mask])
cold = Use_Condition("cold_temp", "Temperature 31-45 \u00b0F.", "fas fa-thermometer-quarter")
cold.set_gear([light_med_jacket, gloves, warm_hat, hiking_boots])
cool = Use_Condition("cool_temp", "Temperature 46-65 \u00b0F.", "fas fa-thermometer-half")
cool.set_gear([light_jacket, hiking_shoes])
moderate = Use_Condition("moderate_temp", "Temperature 66-80 \u00b0F.", "fas fa-thermometer-three-quarters")
moderate.set_gear([hiking_shoes, hiking_boots, moisture_wicking_clothing])
warm = Use_Condition("warm_temp", "Temperature 81-90 \u00b0F.", "fas fa-thermometer-full")
warm.set_gear([moisture_wicking_clothing, shorts, hiking_sandals])
hot = Use_Condition("hot_temp", "Temperature >91 \u00b0F.", "fas fa-thermometer-full")
hot.set_gear([moisture_wicking_clothing, shorts, hiking_sandals, sun_hat, sun_protecetion])
all_use = Use_Condition("all", "All hikers should bring this!", "fas fa-hiking")
all_use.set_gear([daypack, water, snacks, hiking_boots, hiking_shoes,
map_gps, sun_protecetion, first_aid])
rain = Use_Condition("rain", "Chance of rain or snow!", "fas fa-cloud-showers-heavy")
rain.set_gear([rain_jacket, rain_pants, waterproof_socks, waterproof_shoes,
gaiters])
snow = Use_Condition("snow", "There is snow on the ground.", "far fa-snowflake")
snow.set_gear([snowshoes, face_mask, snow_goggles, gaiters, microspikes, trekking_poles])
medium_duration = Use_Condition("medium_duration", "This will be a medium-long hike.", "fas fa-clock")
medium_duration.set_gear([lunch, extra_water])
long_duration = Use_Condition("long_duration", "This will be a long hike.", "fas fa-clock")
long_duration.set_gear([lunch, extra_water, extra_food])
steep = Use_Condition("steep", "Steep trail or high total elevation gain.", "fas fa-mountain")
steep.set_gear([trekking_poles])
wind = Use_Condition("wind", "Its going to be windy!", "fas fa-wind")
wind.set_gear([trekking_poles, windbreaker, face_mask])
use_conditions = {**freezing.get_use_condition(), **cold.get_use_condition(),
**cool.get_use_condition(), **moderate.get_use_condition(),
**warm.get_use_condition(), **hot.get_use_condition(),
**all_use.get_use_condition(), **rain.get_use_condition(),
**snow.get_use_condition(), **medium_duration.get_use_condition(),
**long_duration.get_use_condition(), **steep.get_use_condition(),
**wind.get_use_condition()}
return use_conditions
def add_gear_item(gear_dict, use_condition):
"""
Add all gear items in a use_condition to the gear dictionary.
"""
for gear_item in use_condition["gear"]:
category = use_condition["gear"][gear_item]["category"]
if gear_item not in gear_dict[category]:
# add gear item if not already in gear_dict
gear_dict[category].update({gear_item : [[use_condition["description"], use_condition["icon"]]]})
else:
# if gear item already in gear_dict, add use condition description to
# existing gear item
gear_dict[category][gear_item].append([use_condition["description"], use_condition["icon"]])
return gear_dict
def get_gear(attributes, gear_meta_data):
"""
Create a gear dictionary with gear items whose use conditions match the
given attributes.
"""
# gear dictionary with gear item categories as key
gear_dict = { "clothing" : {}, "footwear" : {}, "food and water" : {},
"gear" : {}, "navigation" : {}, "other" : {} }
for attribute in attributes:
for use_condition in gear_meta_data:
if attribute == use_condition:
gear_dict = add_gear_item(gear_dict, gear_meta_data[use_condition])
# with open("gear_data2.json", "w") as write_file:
# json.dump(gear_dict, write_file, indent=4)
return gear_dict