339 lines
9.1 KiB
Python
339 lines
9.1 KiB
Python
import math
|
|
import numpy as np
|
|
import pandas as pd
|
|
import colorsys
|
|
from django.conf import settings
|
|
|
|
import uuid
|
|
import datetime
|
|
|
|
lbstoN = 4.44822
|
|
|
|
landingpages = (
|
|
('workout_edit_view','Edit View'),
|
|
('workout_workflow_view','Workflow View'),
|
|
)
|
|
|
|
|
|
|
|
workflowmiddlepanel = (
|
|
('panel_statcharts.html','Static Charts'),
|
|
('flexthumbnails.html','Flex Charts'),
|
|
('panel_summary.html','Summary'),
|
|
('panel_map.html','Map'),
|
|
('panel_comments.html','Basic Info and Links'),
|
|
('panel_notes.html','Workout Notes'),
|
|
('panel_shortcomment.html','Comment Link'),
|
|
('panel_middlesocial.html','Social Media Share Buttons'),
|
|
)
|
|
|
|
defaultmiddle = ['panel_middlesocial.html',
|
|
'panel_statcharts.html',
|
|
'flexthumbnails.html',
|
|
'panel_summary.html',
|
|
'panel_map.html']
|
|
|
|
workflowleftpanel = (
|
|
('panel_navigationheader.html','Navigation Header'),
|
|
('panel_editbuttons.html','Edit Workout Button'),
|
|
('panel_delete.html','Delete Workout Button'),
|
|
('panel_export.html','Export Workout Button'),
|
|
('panel_social.html','Social Media Share Buttons'),
|
|
('panel_advancededit.html','Advanced Workout Edit Button'),
|
|
('panel_editintervals.html','Edit Intervals Button'),
|
|
('panel_stats.html','Workout Statistics Button'),
|
|
('panel_flexchart.html','Flex Chart'),
|
|
('panel_staticchart.html','Create Static Charts Buttons'),
|
|
('panel_uploadimage.html','Attach Image'),
|
|
('panel_geekyheader.html','Geeky Header'),
|
|
('panel_editwind.html','Edit Wind Data'),
|
|
('panel_editstream.html','Edit Stream Data'),
|
|
('panel_otwpower.html','Run OTW Power Calculations'),
|
|
('panel_mapview.html','Map'),
|
|
('panel_ranking.html','Ranking View'),
|
|
)
|
|
|
|
defaultleft = [
|
|
'panel_navigationheader.html',
|
|
'panel_editbuttons.html',
|
|
'panel_advancededit.html',
|
|
'panel_editintervals.html',
|
|
'panel_stats.html',
|
|
'panel_staticchart.html',
|
|
'panel_uploadimage.html',
|
|
]
|
|
|
|
coxes_calls = [
|
|
'Sit Ready!',
|
|
"Let's relax the shoulders, and give me a power ten to the finish!",
|
|
"Almost there. Give me ten strokes on the legs!",
|
|
"Let it run!",
|
|
"Don't rush the slides!",
|
|
"Quick hands.",
|
|
"You are clearing the puddles.",
|
|
"Let's push through now. Get me that open water.",
|
|
"We're going for the line now. Power ten on the next.",
|
|
]
|
|
|
|
info_calls = [
|
|
"Please give us a minute to count all those strokes, you've been working hard!",
|
|
"Please give us a minute to count all your strokes."
|
|
]
|
|
|
|
import random
|
|
|
|
def get_call():
|
|
call1 = random.choice(coxes_calls)
|
|
call2 = random.choice(info_calls)
|
|
|
|
call = """<div id="id_sitready" class="successmessage">
|
|
<p>
|
|
%s (%s)
|
|
</p>
|
|
</div>
|
|
""" % (call1, call2)
|
|
|
|
return call
|
|
|
|
def absolute(request):
|
|
urls = {
|
|
'ABSOLUTE_ROOT': request.build_absolute_uri('/')[:-1].strip("/"),
|
|
'ABSOLUTE_ROOT_URL': request.build_absolute_uri('/').strip("/"),
|
|
'PATH':request.build_absolute_uri(),
|
|
}
|
|
|
|
return urls
|
|
|
|
def trcolors(r1,g1,b1,r2,g2,b2):
|
|
r1 = r1/255.
|
|
r2 = r2/255.
|
|
g1 = g1/255.
|
|
g2 = g2/255.
|
|
b2 = b2/255.
|
|
b1 = b1/255.
|
|
h1,s1,v1 = colorsys.rgb_to_hsv(r1,g1,b1)
|
|
h2,s2,v2 = colorsys.rgb_to_hsv(r2,g2,b2)
|
|
|
|
|
|
return 360*h1,360*(h2-h1),s1,(s2-s1),v1,(v2-v1)
|
|
|
|
palettes = {
|
|
'monochrome_blue':(207,-4,0.06,0.89,1.0,-0.38),
|
|
'gold_sunset':(47,-31,.26,-0.12,0.94,-0.5),
|
|
'blue_red':(207,-200,.85,0,.74,-.24),
|
|
'blue_green':(207,-120,.85,0,.75,.25),
|
|
'cyan_green':(192,-50,.08,.65,.98,-.34),
|
|
'cyan_purple':trcolors(237,248,251,136,65,157),
|
|
'green_blue':trcolors(240,249,232,8,104,172),
|
|
'orange_red':trcolors(254,240,217,179,0,0),
|
|
'cyan_blue':trcolors(241,238,246,4,90,141),
|
|
'cyan_green':trcolors(246,239,247,1,108,89),
|
|
'cyan_magenta':trcolors(241,238,246,152,0,67),
|
|
'beige_magenta':trcolors(254,235,226,122,1,119),
|
|
'yellow_green':trcolors(255,255,204,0,104,55),
|
|
'yellow_blue':trcolors(255,255,205,37,52,148),
|
|
'autumn':trcolors(255,255,212,153,52,4),
|
|
'yellow_red':trcolors(255,255,178,189,0,39)
|
|
}
|
|
|
|
rankingdistances = [100,500,1000,2000,5000,6000,10000,21097,42195,100000]
|
|
rankingdurations = []
|
|
rankingdurations.append(datetime.time(minute=1))
|
|
rankingdurations.append(datetime.time(minute=4))
|
|
rankingdurations.append(datetime.time(minute=30))
|
|
rankingdurations.append(datetime.time(hour=1,minute=15))
|
|
rankingdurations.append(datetime.time(hour=1))
|
|
|
|
|
|
def is_ranking_piece(workout):
|
|
if workout.distance in rankingdistances:
|
|
return True
|
|
elif workout.duration in rankingdurations:
|
|
return True
|
|
|
|
return False
|
|
|
|
def range_to_color_hex(groupcols,palette='monochrome_blue'):
|
|
|
|
try:
|
|
plt = palettes[palette]
|
|
except KeyError:
|
|
plt = palettes['monochrome_blue']
|
|
|
|
rgb = [colorsys.hsv_to_rgb((plt[0]+plt[1]*x)/360.,
|
|
plt[2]+plt[3]*x,
|
|
plt[4]+plt[5]*x) for x in groupcols]
|
|
|
|
RGB = [(int(255.*r),int(255.*g),int(255.*b)) for (r, g, b) in rgb]
|
|
colors = ["#%02x%02x%02x" % (r, g, b) for (r, g, b) in RGB]
|
|
|
|
return colors
|
|
|
|
def str2bool(v):
|
|
return v.lower() in ("yes", "true", "t", "1")
|
|
|
|
def uniqify(seq, idfun=None):
|
|
# order preserving
|
|
if idfun is None:
|
|
def idfun(x): return x
|
|
seen = {}
|
|
result = []
|
|
for item in seq:
|
|
marker = idfun(item)
|
|
# in old Python versions:
|
|
# if seen.has_key(marker)
|
|
# but in new ones:
|
|
if marker in seen: continue
|
|
seen[marker] = 1
|
|
result.append(item)
|
|
return result
|
|
|
|
def serialize_list(value,token=','):
|
|
assert(isinstance(value, list) or isinstance(value, tuple) or isinstance(value,np.ndarray))
|
|
return token.join([unicode(s) for s in value])
|
|
|
|
def deserialize_list(value,token=','):
|
|
if isinstance(value, list):
|
|
return value
|
|
elif isinstance(value, np.ndarray):
|
|
return value
|
|
return value.split(token)
|
|
|
|
def geo_distance(lat1,lon1,lat2,lon2):
|
|
""" Approximate distance and bearing between two points
|
|
defined by lat1,lon1 and lat2,lon2
|
|
This is a slight underestimate but is close enough for our purposes,
|
|
We're never moving more than 10 meters between trackpoints
|
|
|
|
Bearing calculation fails if one of the points is a pole.
|
|
(Hey, from the North pole you can walk South, East, North and end up
|
|
on the same spot!)
|
|
|
|
"""
|
|
|
|
# radius of our earth in km --> should be moved to settings if
|
|
# rowing takes off on other planets
|
|
R = 6373.0
|
|
|
|
# pi
|
|
pi = math.pi
|
|
|
|
lat1 = math.radians(lat1)
|
|
lat2 = math.radians(lat2)
|
|
lon1 = math.radians(lon1)
|
|
lon2 = math.radians(lon2)
|
|
|
|
dlon = lon2 - lon1
|
|
dlat = lat2 - lat1
|
|
|
|
a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2
|
|
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
|
|
|
distance = R * c
|
|
|
|
tc1 = math.atan2(math.sin(lon2-lon1)*math.cos(lat2),
|
|
math.cos(lat1)*math.sin(lat2)-math.sin(lat1)*math.cos(lat2)*math.cos(lon2-lon1))
|
|
|
|
tc1 = tc1 % (2*pi)
|
|
|
|
bearing = math.degrees(tc1)
|
|
|
|
return [distance,bearing]
|
|
|
|
|
|
|
|
def isbreakthrough(delta,cpvalues,p0,p1,p2,p3,ratio):
|
|
pwr = abs(p0)/(1+(delta/abs(p2)))
|
|
pwr += abs(p1)/(1+(delta/abs(p3)))
|
|
|
|
dd = 0.25*(ratio-1)
|
|
pwr2 = pwr*(1+dd)
|
|
|
|
pwr *= ratio
|
|
|
|
|
|
delta = delta.values
|
|
cpvalues = cpvalues.values
|
|
|
|
res = np.sum(cpvalues>pwr)
|
|
res2 = np.sum(cpvalues>pwr2)
|
|
|
|
|
|
btdf = pd.DataFrame(
|
|
{
|
|
'delta':delta[cpvalues>pwr],
|
|
'cpvalues':cpvalues[cpvalues>pwr],
|
|
'pwr':pwr[cpvalues>pwr],
|
|
}
|
|
)
|
|
|
|
|
|
btdf.sort_values('delta',axis=0,inplace=True)
|
|
|
|
|
|
return res>1,btdf,res2>1
|
|
|
|
|
|
def myqueue(queue,function,*args,**kwargs):
|
|
if settings.DEBUG:
|
|
kwargs['debug'] = True
|
|
|
|
job = function.delay(*args,**kwargs)
|
|
else:
|
|
job_id = str(uuid.uuid4())
|
|
kwargs['job_id'] = job_id
|
|
kwargs['jobkey'] = job_id
|
|
|
|
job = queue.enqueue(function,*args,**kwargs)
|
|
|
|
return job
|
|
|
|
|
|
from datetime import date
|
|
|
|
def calculate_age(born):
|
|
today = date.today()
|
|
return today.year - born.year - ((today.month, today.day) < (born.month, born.day))
|
|
|
|
def my_dict_from_instance(instance,model):
|
|
thedict = {}
|
|
thedict['id'] = instance.id
|
|
|
|
for f in instance._meta.fields:
|
|
|
|
fname = f.name
|
|
|
|
try:
|
|
verbosename = f.verbose_name
|
|
except:
|
|
verbosename = f.name
|
|
|
|
get_choice = 'get_'+fname+'_display'
|
|
if hasattr( instance, get_choice):
|
|
value = getattr(instance, get_choice)()
|
|
else:
|
|
try:
|
|
value = getattr(instance,fname)
|
|
except AttributeError:
|
|
value = None
|
|
|
|
if f.editable and value:
|
|
thedict[fname] = (verbosename,value)
|
|
|
|
return thedict
|
|
|
|
def wavg(group, avg_name, weight_name):
|
|
""" http://stackoverflow.com/questions/10951341/pandas-dataframe-aggregate-function-using-multiple-columns
|
|
In rare instance, we may not have weights, so just return the mean. Customize this if your business case
|
|
should return otherwise.
|
|
"""
|
|
d = group[avg_name]
|
|
try:
|
|
w = group[weight_name]
|
|
except KeyError:
|
|
return d.mean()
|
|
try:
|
|
return (d * w).sum() / w.sum()
|
|
except ZeroDivisionError:
|
|
return d.mean()
|