Private
Public Access
1
0

Merge branch 'feature/kmlapi' into develop

This commit is contained in:
2024-03-01 09:04:46 +01:00
15 changed files with 1331 additions and 878 deletions
+157 -16
View File
@@ -4,6 +4,7 @@ from rowers.models import (
Rower, Workout, Rower, Workout,
GeoPoint, GeoPolygon, GeoCourse, GeoPoint, GeoPolygon, GeoCourse,
course_length, course_coord_center, course_coord_maxmin, course_length, course_coord_center, course_coord_maxmin,
course_coord_crewnerd_navigation,
polygon_coord_center, PlannedSession, polygon_coord_center, PlannedSession,
polygon_to_path, coordinate_in_path polygon_to_path, coordinate_in_path
) )
@@ -22,6 +23,7 @@ import time
from django.db import IntegrityError from django.db import IntegrityError
import uuid import uuid
from django.conf import settings from django.conf import settings
import math
import geocoder import geocoder
@@ -120,6 +122,50 @@ xmlns_uris_dict = {'gx': 'http://www.google.com/kml/ext/2.2',
'': "http://www.opengis.net/kml/2.2"} '': "http://www.opengis.net/kml/2.2"}
def get_polar_angle(point, reference_point):
"""
Calculate the polar angle of a point with respect to a reference point.
"""
try:
delta_x = point.longitude - reference_point.longitude
delta_y = point.latitude - reference_point.latitude
except AttributeError:
delta_x = point['longitude'] - reference_point.longitude
delta_y = point['latitude'] - reference_point.latitude
return math.atan2(delta_y, delta_x)
class Coordinate:
def __init__(self, latitude, longitude):
self.latitude = latitude
self.longitude = longitude
def get_coordinates_centerpoint(coordinates):
try:
centroid_latitude = sum(coord.latitude for coord in coordinates) / len(coordinates)
centroid_longitude = sum(coord.longitude for coord in coordinates) / len(coordinates)
centroid = Coordinate(centroid_latitude, centroid_longitude)
except AttributeError:
centroid_latitude = sum(coord['latitude'] for coord in coordinates) / len(coordinates)
centroid_longitude = sum(coord['longitude'] for coord in coordinates) / len(coordinates)
centroid = Coordinate(centroid_latitude, centroid_longitude)
return centroid
def sort_coordinates_ccw(coordinates):
"""
Sort coordinates in counterclockwise order.
"""
# Find the reference point (centroid)
centroid = get_coordinates_centerpoint(coordinates)
# Sort coordinates based on polar angle with respect to the centroid
sorted_coords = sorted(coordinates, key=lambda coord: get_polar_angle(coord, centroid))
return sorted_coords
def crewnerdcourse(doc): def crewnerdcourse(doc):
courses = [] courses = []
for course in doc: for course in doc:
@@ -162,6 +208,8 @@ def get_polygons(polygonpms):
'latitude': float(coordinates[1]), 'latitude': float(coordinates[1]),
}) })
points = sort_coordinates_ccw(points)
polygons.append({ polygons.append({
'name': name, 'name': name,
'points': points 'points': points
@@ -169,34 +217,45 @@ def get_polygons(polygonpms):
return polygons return polygons
def crewnerdify(polygons):
polygons[0].name = "Start"
polygons[len(polygons)-1].name = "Finish"
def coursetokml(course): if len(polygons) > 2:
top = Element('kml') for i in range(1,len(polygons)-1):
for prefix, uri in xmlns_uris_dict.items(): polygons[i].name = 'WP{n}'.format(n=polygons[i].order_in_course)
if prefix != '':
top.attrib['xmlns:' + prefix] = uri
else:
top.attrib['xmlns'] = uri
document = SubElement(top, 'Document') return polygons
name = SubElement(document, 'name')
name.text = 'Courses.kml' def removewhitespace(s):
folder = SubElement(document, 'Folder') regels = s.split('\n')
foldername = SubElement(folder, 'name') regels = [regel for regel in regels if regel.strip()]
foldername.text = 'Courses' return "\n".join(regels)
folder2 = SubElement(folder, 'Folder')
def getcoursefolder(course, document, cn=False):
folder2 = SubElement(document, 'Folder', id="{id}".format(id=course.id))
coursename = SubElement(folder2, 'name') coursename = SubElement(folder2, 'name')
coursename.text = course.name coursename.text = course.name
open = SubElement(folder2, 'open') openst = SubElement(folder2, 'open')
open.text = '1' openst.text = '1'
coursedescription = SubElement(folder2, 'description')
if len(removewhitespace(course.notes)):
coursedescription.text = "Rowsandall.com\n" + removewhitespace(course.notes)
else:
coursedescription.text = "Rowsandall.com " + course.name
polygons = GeoPolygon.objects.filter( polygons = GeoPolygon.objects.filter(
course=course).order_by("order_in_course") course=course).order_by("order_in_course")
if cn:
polygons = crewnerdify(polygons)
for polygon in polygons: for polygon in polygons:
placemark = SubElement(folder2, 'Placemark') placemark = SubElement(folder2, 'Placemark')
polygonname = SubElement(placemark, 'name') polygonname = SubElement(placemark, 'name')
polygonname.text = polygon.name polygonname.text = polygon.name
polygonstyle = SubElement(placemark, 'styleUrl')
polygonstyle.text = "#default0"
p = SubElement(placemark, 'Polygon') p = SubElement(placemark, 'Polygon')
tessellate = SubElement(p, 'tessellate') tessellate = SubElement(p, 'tessellate')
tessellate.text = '1' tessellate.text = '1'
@@ -206,6 +265,7 @@ def coursetokml(course):
coordinates.text = '' coordinates.text = ''
points = GeoPoint.objects.filter( points = GeoPoint.objects.filter(
polygon=polygon).order_by("order_in_poly") polygon=polygon).order_by("order_in_poly")
points = sort_coordinates_ccw(points)
for point in points: for point in points:
coordinates.text += '{lon},{lat},0 '.format( coordinates.text += '{lon},{lat},0 '.format(
lat=point.latitude, lat=point.latitude,
@@ -215,6 +275,87 @@ def coursetokml(course):
lat=points[0].latitude, lat=points[0].latitude,
lon=points[0].longitude, lon=points[0].longitude,
) )
return folder2
def getstyle(document, id):
style = SubElement(document,'Style', id=id)
iconstyle = SubElement(style, 'IconStyle')
scale = SubElement(iconstyle, 'scale')
scale.text = "1.2"
linestyle = SubElement(style, 'LineStyle')
linestylecolor = SubElement(linestyle, 'color')
linestylecolor.text = "ff00ffff"
polystyle = SubElement(style, 'PolyStyle')
polystylecolor = SubElement(polystyle, 'color')
polystylecolor.text = "ff7fffff"
return style
def getstylemap(document, id):
stylemap = SubElement(document, 'StyleMap', id=id)
pair1 = SubElement(stylemap, 'Pair')
pair1key = SubElement(pair1, 'key')
pair1key.text = "normal"
pair1styleurl = SubElement(pair1, 'styleUrl')
pair1styleurl.text = "#default"
pair2 = SubElement(stylemap, 'Pair')
pair2key = SubElement(pair2, 'key')
pair2key.text = "highlight"
pair2styleurl = SubElement(pair2, 'styleUrl')
pair2styleurl.text = "#hl"
return stylemap
def coursestokml(courseids, cn=False):
courses = GeoCourse.objects.filter(id__in=courseids)
top = Element('kml')
for prefix, uri in xmlns_uris_dict.items():
if prefix != '':
top.attrib['xmlns:' + prefix] = uri
else:
top.attrib['xmlns'] = uri
document = SubElement(top, 'Document')
name = SubElement(document, 'name')
name.text = 'courses'
opendoc = SubElement(document,'open')
opendoc.text = '1'
style = getstyle(document, "default")
stylemap = getstylemap(document, 'default0')
stylehl = getstyle(document, "hl")
for course in courses:
coursefolder = getcoursefolder(course, document, cn=cn)
return prettify(top)
def coursetokml(course,cn=False):
top = Element('kml')
for prefix, uri in xmlns_uris_dict.items():
if prefix != '':
top.attrib['xmlns:' + prefix] = uri
else:
top.attrib['xmlns'] = uri
document = SubElement(top, 'Document')
name = SubElement(document, 'name')
name.text = 'courses'
opendoc = SubElement(document,'open')
opendoc.text = '1'
style = getstyle(document, "default")
stylemap = getstylemap(document, 'default0')
stylehl = getstyle(document, "hl")
coursefolder = getcoursefolder(course, document, cn=cn)
return prettify(top) return prettify(top)
+3 -1
View File
@@ -33,7 +33,7 @@ import datetime
from rowers import mytypes from rowers import mytypes
from rowers.courses import ( from rowers.courses import (
course_coord_center, course_coord_maxmin, course_coord_center, course_coord_maxmin,
polygon_coord_center polygon_coord_center, course_coord_crewnerd_navigation,
) )
from django.conf import settings from django.conf import settings
from collections import OrderedDict from collections import OrderedDict
@@ -2165,6 +2165,8 @@ def interactive_histoall(theworkouts, histoparam, includereststrokes,
def course_map(course): def course_map(course):
latmean, lonmean, coordinates = course_coord_center(course) latmean, lonmean, coordinates = course_coord_center(course)
if course.with_cn_nav_waypoints:
latmean, lonmean, coordinates = course_coord_crewnerd_navigation(course)
lat_min, lat_max, long_min, long_max = course_coord_maxmin(course) lat_min, lat_max, long_min, long_max = course_coord_maxmin(course)
coordinates = course_spline(coordinates) coordinates = course_spline(coordinates)
+77 -1
View File
@@ -8,7 +8,7 @@ from rowers.utils import (
steps_read_fit, steps_write_fit, ps_dict_order, uniqify steps_read_fit, steps_write_fit, ps_dict_order, uniqify
) )
from rowers.metrics import axlabels from rowers.metrics import axlabels
from rowers.utils import geo_distance from rowers.utils import geo_distance, move_one_meter
from rowers.formfields import * from rowers.formfields import *
from rowers.database import * from rowers.database import *
import uuid import uuid
@@ -594,6 +594,64 @@ def course_spline(coordinates):
return newcoordinates return newcoordinates
def polygon_nearest_point(polygon, latitude, longitude,debug=False):
points = GeoPoint.objects.filter(polygon=polygon)
points = sorted(points, key = lambda p: geo_distance(p.latitude, p.longitude, latitude, longitude))
#if debug:
# for p in points:
# print(p,p.latitude, p.longitude, latitude, longitude, geo_distance(p.latitude, p.longitude, latitude, longitude))
return points[0].latitude, points[0].longitude
def polygon_exit_point(polygon, lat1, lon1, lat2, lon2):
dist, bearing = geo_distance(lat1, lon1, lat2, lon2)
dirveclat = (lat2-lat1)/dist
dirveclon = (lon2-lon1)/dist
newlat, newlon = move_one_meter(lat2, lon2, bearing)
path = polygon_to_path(polygon)
while path.contains_points([(newlat, newlon)])[0]:
newlat, newlon = move_one_meter(newlat, newlon, bearing)
return newlat, newlon
def course_coord_crewnerd_navigation(course):
polygons = GeoPolygon.objects.filter(
course=course).order_by("order_in_course")
latitudes = []
longitudes = []
latitude, longitude = polygon_coord_center(polygons[0])
latitudes.append(latitude)
longitudes.append(longitude)
debug = True
for p in polygons[1:]:
oldlat = latitude
oldlon = longitude
latitude, longitude = polygon_nearest_point(p,latitude,longitude, debug=debug)
debug = False
latitudes.append(latitude)
longitudes.append(longitude)
latitude, longitude = polygon_exit_point(p, oldlat, oldlon, latitude, longitude)
latitudes.append(latitude)
longitudes.append(longitude)
latitude = pd.Series(latitudes).median()
longitude = pd.Series(longitudes).median()
coordinates = pd.DataFrame({
'latitude': latitudes,
'longitude': longitudes,
})
return latitude, longitude, coordinates
def course_coord_center(course): def course_coord_center(course):
@@ -1590,6 +1648,24 @@ class GeoCourse(models.Model):
def coord(self): def coord(self):
return course_coord_center(self) return course_coord_center(self)
@property
def with_cn_nav_waypoints(self):
polygons = GeoPolygon.objects.filter(course=self).order_by("order_in_course")
if polygons[0].name != "Start":
return False
if polygons[len(polygons)-1].name != "Finish":
return False
for i in range(1,len(polygons)-1):
if polygons[i].name[0:2].lower() != 'wp':
return False
try:
getal = float(polygons[i].name[2:])
except ValueError:
return False
return True
class GeoCourseEditForm(ModelForm): class GeoCourseEditForm(ModelForm):
class Meta: class Meta:
+22 -22
View File
@@ -94,7 +94,7 @@ SITE_URL_DEV = CFG['site_url']
PROGRESS_CACHE_SECRET = CFG['progress_cache_secret'] PROGRESS_CACHE_SECRET = CFG['progress_cache_secret']
try: try:
SETTINGS_NAME = CFG['settings_name'] SETTINGS_NAME = CFG['settings_name']
except KeyError: except KeyError: # pragma: no cover
SETTINGS_NAME = 'rowsandall_ap.settings' SETTINGS_NAME = 'rowsandall_ap.settings'
try: try:
@@ -321,7 +321,7 @@ def summaryfromsplitdata(splitdata, data, filename, sep='|', workouttype='rower'
return sums, sa, results return sums, sa, results
@app.task @app.task
def handle_post_workout_api(uploadoptions, debug=False, **kwargs): def handle_post_workout_api(uploadoptions, debug=False, **kwargs): # pragma: no cover
session = requests.session() session = requests.session()
newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'} newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'}
session.headers.update(newHeaders) session.headers.update(newHeaders)
@@ -335,14 +335,14 @@ def handle_post_workout_api(uploadoptions, debug=False, **kwargs):
@app.task @app.task
def handle_remove_workouts_team(ws, t, debug=False, **kwargs): def handle_remove_workouts_team(ws, t, debug=False, **kwargs): # pragma: no cover
for w in ws: for w in ws:
w.team.remove(t) w.team.remove(t)
return 1 return 1
@app.task @app.task
def handle_add_workouts_team(ws, t, debug=False, **kwargs): def handle_add_workouts_team(ws, t, debug=False, **kwargs): # pragma: no cover
for w in ws: for w in ws:
w.team.add(t) w.team.add(t)
@@ -350,7 +350,7 @@ def handle_add_workouts_team(ws, t, debug=False, **kwargs):
return 1 return 1
def uploadactivity(access_token, filename, description='', def uploadactivity(access_token, filename, description='',
name='Rowsandall.com workout'): name='Rowsandall.com workout'): # pragma: no cover
data_gz = BytesIO() data_gz = BytesIO()
try: try:
@@ -393,7 +393,7 @@ def uploadactivity(access_token, filename, description='',
@app.task @app.task
def check_tp_workout_id(workout, location, attempts=5, debug=False, **kwargs): def check_tp_workout_id(workout, location, attempts=5, debug=False, **kwargs): # pragma: no cover
authorizationstring = str('Bearer ' + workout.user.tptoken) authorizationstring = str('Bearer ' + workout.user.tptoken)
headers = {'Authorization': authorizationstring, headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal', 'user-agent': 'sanderroosendaal',
@@ -411,7 +411,7 @@ def check_tp_workout_id(workout, location, attempts=5, debug=False, **kwargs):
return 1 return 1
@app.task @app.task
def handle_workout_tp_upload(w, thetoken, tcxfilename, debug=False, **kwargs): def handle_workout_tp_upload(w, thetoken, tcxfilename, debug=False, **kwargs): # pragma: no cover
tpid = 0 tpid = 0
r = w.user r = w.user
if not tcxfilename: if not tcxfilename:
@@ -443,7 +443,7 @@ def handle_workout_tp_upload(w, thetoken, tcxfilename, debug=False, **kwargs):
return tpid return tpid
@app.task @app.task
def instroke_static(w, metric, debug=False, **kwargs): def instroke_static(w, metric, debug=False, **kwargs): # pragma: no cover
f1 = w.csvfilename[6:-4] f1 = w.csvfilename[6:-4]
rowdata = rdata(csvfile=w.csvfilename) rowdata = rdata(csvfile=w.csvfilename)
@@ -512,7 +512,7 @@ def handle_c2_sync(workoutid, url, headers, data, debug=False, **kwargs):
try: try:
workout = Workout.objects.get(id=workoutid) workout = Workout.objects.get(id=workoutid)
except Workout.DoesNotExist: except Workout.DoesNotExist: # pragma: no cover
dologging('c2_log.log','failed for c2id {c2id}'.format(c2id=c2id)) dologging('c2_log.log','failed for c2id {c2id}'.format(c2id=c2id))
return 0 return 0
@@ -531,7 +531,7 @@ def handle_c2_sync(workoutid, url, headers, data, debug=False, **kwargs):
return 1 return 1
def splitstdata(lijst): def splitstdata(lijst): # pragma: no cover
t = [] t = []
latlong = [] latlong = []
while len(lijst) >= 2: while len(lijst) >= 2:
@@ -543,7 +543,7 @@ def splitstdata(lijst):
@app.task @app.task
def handle_sporttracks_workout_from_data(user, importid, source, def handle_sporttracks_workout_from_data(user, importid, source,
workoutsource, debug=False, **kwargs): workoutsource, debug=False, **kwargs): # pragma: no cover
r = user.rower r = user.rower
authorizationstring = str('Bearer ' + r.sporttrackstoken) authorizationstring = str('Bearer ' + r.sporttrackstoken)
@@ -812,7 +812,7 @@ def handle_strava_sync(stravatoken,
if not failed: if not failed:
try: try:
workout = Workout.objects.get(id=workoutid) workout = Workout.objects.get(id=workoutid)
except Workout.DoesNotExist: except Workout.DoesNotExist: # pragma: no cover
return 0 return 0
workout.uploadedtostrava = res.id workout.uploadedtostrava = res.id
@@ -1562,7 +1562,7 @@ def handle_calctrimp(id,
try: try:
workout = Workout.objects.get(id=id) workout = Workout.objects.get(id=id)
except Workout.DoesNotExist: except Workout.DoesNotExist: # pragma: no cover
dologging('metrics.log','Could not find workout {id}'.format(id=id)) dologging('metrics.log','Could not find workout {id}'.format(id=id))
return 0 return 0
@@ -2037,7 +2037,7 @@ def handle_sendemail_expired(useremail, userfirstname, userlastname, expireddate
return 1 return 1
@app.task @app.task
def handle_sendemail_newftp(rower,power,mode, **kwargs): def handle_sendemail_newftp(rower,power,mode, **kwargs): # pragma: no cover
subject = "You may want to update your FTP on rowsandall.com" subject = "You may want to update your FTP on rowsandall.com"
from_email = 'Rowsandall <info@rowsandall.com>' from_email = 'Rowsandall <info@rowsandall.com>'
@@ -2069,7 +2069,7 @@ def handle_sendemail_breakthrough(workoutid, useremail,
lastname = '' lastname = ''
if surname: if surname: # pragma: no cover
lastname = userlastname lastname = userlastname
tablevalues = [ tablevalues = [
@@ -2122,7 +2122,7 @@ def handle_sendemail_hard(workoutid, useremail,
] ]
lastname = '' lastname = ''
if surname: if surname: # pragma: no cover
lastname = userlastname lastname = userlastname
# send email with attachment # send email with attachment
@@ -3414,7 +3414,7 @@ def handle_nk_async_workout(alldata, userid, nktoken, nkid, delaysec, defaulttim
jsonData = response.json() jsonData = response.json()
try: try:
strokeData = jsonData[str(nkid)] strokeData = jsonData[str(nkid)]
except KeyError: except KeyError: # pragma: no cover
dologging('nklog.log','Could not find strokeData') dologging('nklog.log','Could not find strokeData')
return 0 return 0
@@ -3424,7 +3424,7 @@ def handle_nk_async_workout(alldata, userid, nktoken, nkid, delaysec, defaulttim
if oarlockSessions: if oarlockSessions:
oarlocksession = oarlockSessions[0] oarlocksession = oarlockSessions[0]
seatNumber = oarlocksession['seatNumber'] seatNumber = oarlocksession['seatNumber']
except KeyError: except KeyError: # pragma: no cover
pass pass
df = strokeDataToDf(strokeData, seatIndex=seatNumber) df = strokeDataToDf(strokeData, seatIndex=seatNumber)
@@ -3439,14 +3439,14 @@ def handle_nk_async_workout(alldata, userid, nktoken, nkid, delaysec, defaulttim
workoutid, error = add_workout_from_data(userid, nkid, data, df) workoutid, error = add_workout_from_data(userid, nkid, data, df)
# dologging('nklog.log','NK Workout ID {id}'.format(id=workoutid)) # dologging('nklog.log','NK Workout ID {id}'.format(id=workoutid))
if workoutid == 0: if workoutid == 0: # pragma: no cover
return 0 return 0
try: try:
workout = Workout.objects.get(id=workoutid) workout = Workout.objects.get(id=workoutid)
newnkid = workout.uploadedtonk newnkid = workout.uploadedtonk
sr = create_or_update_syncrecord(workout.user, workout, nkid=newnkid) sr = create_or_update_syncrecord(workout.user, workout, nkid=newnkid)
except Workout.DoesNotExist: except Workout.DoesNotExist: # pragma: no cover
pass pass
return workoutid return workoutid
@@ -3775,7 +3775,7 @@ def handle_c2_async_workout(alldata, userid, c2token, c2id, delaysec,
return workoutid return workoutid
@app.task @app.task
def fetch_rojabo_session(id,alldata,userid,rowerid,debug=False, **kwargs): def fetch_rojabo_session(id,alldata,userid,rowerid,debug=False, **kwargs): # pragma: no cover
try: try:
item = alldata[id] item = alldata[id]
except KeyError: except KeyError:
@@ -3880,7 +3880,7 @@ def fetch_strava_workout(stravatoken, oauth_data, stravaid, csvfilename, userid,
wsize = round(5./dt) wsize = round(5./dt)
velo2 = ewmovingaverage(velo, wsize) velo2 = ewmovingaverage(velo, wsize)
except ValueError: except ValueError: # pragma: no cover
velo2 = velo velo2 = velo
if coords is not None: if coords is not None:
+29 -22
View File
@@ -23,30 +23,36 @@
<li class="grid_3"> <li class="grid_3">
{% if courses %} {% if courses %}
<p> <p>
<table width="100%" class="listtable shortpadded"> <form enctype="multipart/form-data" method="post">
<thead> {% csrf_token %}
<tr> <input name="courses" type="submit" value="Download selected courses">
<th> Country</th> <table width="100%" class="listtable shortpadded">
<th> Name</th> <thead>
<th> Distance</th> <tr>
</tr> <th> Download</th>
</thead> <th> Country</th>
<tbody> <th> Name</th>
{% for course in courses %} <th> Distance</th>
<tr> </tr>
<td> {{ course.country }} </td> </thead>
<td> <tbody>
<a href="/rowers/courses/{{ course.id }}/">{{ course.name }}</a> {% for course in courses %}
</td> <tr>
<td> <td> <input type="checkbox" value={{ course.id }} name="courseid"> </td>
{{ course.distance }} m <td> {{ course.country }} </td>
</td> <td>
<a href="/rowers/courses/{{ course.id }}/">{{ course.name }}</a>
</td>
<td>
{{ course.distance }} m
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</form>
</p> </p>
{% else %} {% else %}
<p> No courses found </p> <p> No courses found </p>
@@ -106,6 +112,7 @@
<p>CrewNerd has published a nice video tutorial of the process. <p>CrewNerd has published a nice video tutorial of the process.
<a href="https://youtu.be/whhWFmMJbhM">Click here</a> to see the video. The part <a href="https://youtu.be/whhWFmMJbhM">Click here</a> to see the video. The part
we're interested in starts at 2:05. we're interested in starts at 2:05.
There is also a <a href="https://performancephones.com/custom-courses/">written tutorial</a> by CrewNerd.
</p> </p>
<p> <p>
File diff suppressed because it is too large Load Diff
+22
View File
@@ -7,6 +7,7 @@ from __future__ import unicode_literals
from .statements import * from .statements import *
nu = datetime.datetime.now() nu = datetime.datetime.now()
import rowers.courses as courses
tested = [ tested = [
@@ -27,6 +28,14 @@ class URLTests(TestCase):
r = Rower.objects.create(user=u,rowerplan='coach',gdproptin=True, ftpset=True, r = Rower.objects.create(user=u,rowerplan='coach',gdproptin=True, ftpset=True,
gdproptindate=timezone.now()) gdproptindate=timezone.now())
self.c = Client() self.c = Client()
cs = courses.kmltocourse('rowers/tests/testdata/thyro.kml')
course = cs[0]
cname = course['name']
cnotes = course['description']
polygons = course['polygons']
self.ThyroBaantje = courses.createcourse(r,cname,polygons,notes=cnotes)
self.ThyroBaantje.save()
self.nu = datetime.datetime.now() self.nu = datetime.datetime.now()
filename = 'rowers/tests/testdata/testdata.csv' filename = 'rowers/tests/testdata/testdata.csv'
@@ -168,6 +177,19 @@ class URLTests(TestCase):
'/rowers/workout/upload/team/', '/rowers/workout/upload/team/',
'/rowers/workouts-join/', '/rowers/workouts-join/',
'/rowers/workouts-join-select/', '/rowers/workouts-join-select/',
'/rowers/api/courses/',
'/rowers/api/courses/?name=Brno',
'/rowers/api/courses/?course_distance=2000',
'/rowers/api/courses/?course_distance=aap',
'/rowers/api/courses/?distance_from=52&latitude=42&longitude=7',
'/rowers/api/courses/?distance_from=50&latitude=42&longitude=-aap',
'/rowers/api/courses/',
'/rowers/api/courses/1/',
'/rowers/list-workouts/?selectworkouts=true',
'/rowers/api/courses/kml/',
'/rowers/api/courses/kml/?id=1',
'/rowers/api/courses/kml?id=1&id=4/',
'/rowers/api/courses/kml/?id=aap',
] ]
Binary file not shown.
+3
View File
@@ -251,6 +251,9 @@ urlpatterns = [
name='strokedatajson_v3'), name='strokedatajson_v3'),
re_path(r'^api/TCX/workouts/$', views.strokedata_tcx, re_path(r'^api/TCX/workouts/$', views.strokedata_tcx,
name='strokedata_tcx'), name='strokedata_tcx'),
re_path(r'^api/courses/$', views.course_list, name='course_list'),
re_path(r'^api/courses/(?P<id>\d+)/$', views.get_crewnerd_kml, name='get_crewnerd_kml'),
re_path(r'^api/courses/kml/$', views.get_crewnerd_multiple, name='get_crewnerd_multiple'),
re_path(r'^500v/$', views.error500_view, name='error500_view'), re_path(r'^500v/$', views.error500_view, name='error500_view'),
re_path(r'^500q/$', views.servererror_view, name='servererror_view'), re_path(r'^500q/$', views.servererror_view, name='servererror_view'),
path('502/', TemplateView.as_view(template_name='502.html'), name='502'), path('502/', TemplateView.as_view(template_name='502.html'), name='502'),
+25
View File
@@ -312,6 +312,31 @@ def geo_distance(lat1, lon1, lat2, lon2):
return [distance, bearing] return [distance, bearing]
def move_one_meter(latitude, longitude, bearing):
# Earth radius in meters
earth_radius = 6371000
# Convert latitude and longitude from degrees to radians
lat_rad = math.radians(latitude)
lon_rad = math.radians(longitude)
# Convert bearing from degrees to radians
bearing_rad = math.radians(bearing)
# Calculate the new latitude
new_lat = math.asin(math.sin(lat_rad) * math.cos(1/earth_radius) +
math.cos(lat_rad) * math.sin(1/earth_radius) * math.cos(bearing_rad))
# Calculate the new longitude
new_lon = lon_rad + math.atan2(math.sin(bearing_rad) * math.sin(1/earth_radius) * math.cos(lat_rad),
math.cos(1/earth_radius) - math.sin(lat_rad) * math.sin(new_lat))
# Convert new latitude and longitude from radians to degrees
new_lat = math.degrees(new_lat)
new_lon = math.degrees(new_lon)
return new_lat, new_lon
def isbreakthrough(delta, cpvalues, p0, p1, p2, p3, ratio): def isbreakthrough(delta, cpvalues, p0, p1, p2, p3, ratio):
pwr = abs(p0)/(1+(delta/abs(p2))) pwr = abs(p0)/(1+(delta/abs(p2)))
+3 -2
View File
@@ -2680,9 +2680,10 @@ def history_view(request, userid=0):
last28 = timezone.now() - datetime.timedelta(days=28) last28 = timezone.now() - datetime.timedelta(days=28)
today = timezone.now() today = timezone.now()
lastyear = today - datetime.timedelta(days=365)
lastyear = datetime.datetime( #lastyear = datetime.datetime(
year=today.year - 1, month=today.month, day=today.day) # year=today.year - 1, month=today.month, day=today.day)
firstmay = datetime.datetime( firstmay = datetime.datetime(
year=today.year, month=5, day=1).astimezone(usertimezone) year=today.year, month=5, day=1).astimezone(usertimezone)
+129 -16
View File
@@ -1,6 +1,7 @@
from rowers.views.statements import * from rowers.views.statements import *
from rowers.tasks import handle_calctrimp from rowers.tasks import handle_calctrimp
from rowers.opaque import encoder from rowers.opaque import encoder
from rowers.courses import coursetokml, coursestokml
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
import arrow import arrow
@@ -11,6 +12,8 @@ from rowers.dataroutines import get_workouttype_from_tcx, get_startdate_time_zon
from rest_framework.decorators import parser_classes from rest_framework.decorators import parser_classes
from rest_framework.parsers import BaseParser from rest_framework.parsers import BaseParser
from rowers.utils import geo_distance
from datetime import datetime as dt from datetime import datetime as dt
import rowingdata.tcxtools as tcxtools import rowingdata.tcxtools as tcxtools
@@ -24,7 +27,7 @@ class XMLParser(BaseParser):
dologging("apilog.log", "XML Parser") dologging("apilog.log", "XML Parser")
try: try:
s = ET.parse(stream).getroot() s = ET.parse(stream).getroot()
except Exception as e: except Exception as e: # pragma: no cover
dologging("apilog.log",e) dologging("apilog.log",e)
return HttpResponse(status=500) return HttpResponse(status=500)
return s return s
@@ -228,13 +231,123 @@ def strokedataform_v2(request, id=0):
def part_of_day(hour): def part_of_day(hour):
if 5 <= hour < 12: if 5 <= hour < 12:
return "Morning" return "Morning"
elif 12 <= hour < 18: elif 12 <= hour < 18: # pragma: no cover
return "Afternoon" return "Afternoon"
elif 18 <= hour < 24: elif 18 <= hour < 24: # pragma: no cover
return "Evening" return "Evening"
else: else: # pragma: no cover
return "Night" return "Night"
# KML API views
"""
- Get a list of courses OK
- Nearby a certain coordinate
- Filtered by
- Get a (KML) course (in response.content rather than as attachment) OK
- Get multiple courses as one KML in response.content OK
- GET with parameters?
Optional, not for CN
- Create one or more new courses from KML
- Should check for duplicates (Placemark ID)
- Update one or more new courses from KML
"""
@api_view(["GET"])
@permission_classes([AllowAny])
def course_list(request):
if request.method != 'GET': # pragma: no cover
dologging('apilog.log','{m} request to KML endpoint'.format(m=request.method))
return HttpResponseNotAllowed("Method not supported")
query_data = {}
name = request.GET.get('name')
distance = request.GET.get('course_distance')
latitude = request.GET.get('latitude')
longitude = request.GET.get('longitude')
distance_from = request.GET.get('distance_from')
if name is not None:
query_data['name__contains'] = name
if distance is not None:
try:
query_data['distance__lte'] = int(distance)+50
query_data['distance__gte'] = int(distance)-50
except:
pass
courses = GeoCourse.objects.filter(**query_data)
if latitude is not None and longitude is not None and distance_from is not None:
try:
newlist = []
for c in courses:
distance = geo_distance(float(latitude), float(longitude), c.coord[0], c.coord[1])[0]
if distance < float(distance_from): # pragma: no cover
newlist.append(c)
courses = newlist
except ValueError:
pass
courselist = []
for c in courses:
d = {
'id': c.id,
'name':c.name,
'country':c.country,
'distance':c.distance,
'notes':c.notes,
'location':(c.coord[0], c.coord[1]),
}
courselist.append(d)
response_dict = {'courses': courselist}
return JsonResponse(response_dict, content_type='application/json; charset=utf8')
@api_view(["GET"])
@permission_classes([AllowAny])
def get_crewnerd_kml(request,id=0):
if request.method != 'GET': # pragma: no cover
dologging('apilog.log','{m} request to CrewNerd KML endpoint'.format(m=request.method))
return HttpResponseNotAllowed("Method not supported")
try:
c = GeoCourse.objects.get(id=id)
except GeoCourse.DoesNotExist: # pragma: no cover
raise Http404("This course does not exist")
kml = coursetokml(c, cn=True)
return HttpResponse(kml)
#@csrf_exempt
@api_view(["GET"])
@permission_classes([AllowAny])
def get_crewnerd_multiple(request):
if request.method != 'GET': # pragma: no cover
dologging('apilog.log','{m} request to CrewNerd KML endpoint'.format(m=request.method))
return HttpResponseNotAllowed("Method not supported")
ids = request.GET.get('id')
if ids is not None:
tdict = dict(request.GET.lists())
idsnew = []
for id in tdict['id']:
try:
idsnew.append(int(id))
except ValueError:
pass
ids = idsnew
else:
gcs = GeoCourse.objects.all()
ids = [c.id for c in gcs]
kml = coursestokml(ids, cn=True)
return HttpResponse(kml)
# Stroke data views
@csrf_exempt @csrf_exempt
@login_required() @login_required()
@api_view(["POST"]) @api_view(["POST"])
@@ -244,11 +357,11 @@ def strokedata_tcx(request):
""" """
Upload a TCX file through API Upload a TCX file through API
""" """
if request.method != 'POST': if request.method != 'POST': # pragma: no cover
dologging('apilog.log','GET request to TCX endpoint') dologging('apilog.log','GET request to TCX endpoint')
return HttpResponseNotAllowed("Method not supported") # pragma: no cover return HttpResponseNotAllowed("Method not supported")
if 'application/xml' not in request.content_type.lower(): if 'application/xml' not in request.content_type.lower(): # pragma: no cover
dologging('apilog.log','POST data not application/xml, request to TCX endpoint') dologging('apilog.log','POST data not application/xml, request to TCX endpoint')
dologging('apilog.log', request.content_type.lower()) dologging('apilog.log', request.content_type.lower())
return HttpResponseNotAllowed("Need application/xml") return HttpResponseNotAllowed("Need application/xml")
@@ -275,7 +388,7 @@ def strokedata_tcx(request):
lap_duration_seconds = float(lap_duration_node.text) lap_duration_seconds = float(lap_duration_node.text)
total_duration += lap_duration_seconds total_duration += lap_duration_seconds
except Exception as e: except Exception as e: # pragma: no cover
dologging('apilog.log','TCX') dologging('apilog.log','TCX')
dologging('apilog.log',e) dologging('apilog.log',e)
return HttpResponseNotAllowed("Could not parse TCX data") return HttpResponseNotAllowed("Could not parse TCX data")
@@ -329,7 +442,7 @@ def strokedata_tcx(request):
"workout id": workoutid, "workout id": workoutid,
"status": "success", "status": "success",
}) })
except Exception as e: except Exception as e: # pragma: no cover
dologging('apilog.log','TCX API endpoint') dologging('apilog.log','TCX API endpoint')
dologging('apilog.log',e) dologging('apilog.log',e)
return HttpResponse(status=500) return HttpResponse(status=500)
@@ -382,7 +495,7 @@ def strokedatajson_v3(request):
title = request.data.get('name','') title = request.data.get('name','')
try: try:
elapsedTime = request.data['elapsedTime'] elapsedTime = request.data['elapsedTime']
except KeyError: except KeyError: # pragma: no cover
try: try:
duration = request.data['duration'] duration = request.data['duration']
try: try:
@@ -394,7 +507,7 @@ def strokedatajson_v3(request):
return HttpResponse("Missing Elapsed Time", status=400) return HttpResponse("Missing Elapsed Time", status=400)
try: try:
totalDistance = request.data['distance'] totalDistance = request.data['distance']
except KeyError: except KeyError: # pragma: no cover
return HttpResponse("Missing Total Distance", status=400) return HttpResponse("Missing Total Distance", status=400)
timeZone = request.data.get('timezone','UTC') timeZone = request.data.get('timezone','UTC')
workouttype = request.data.get('workouttype','rower') workouttype = request.data.get('workouttype','rower')
@@ -415,12 +528,12 @@ def strokedatajson_v3(request):
df = pd.DataFrame() df = pd.DataFrame()
try: try:
strokes = request.data['strokes'] strokes = request.data['strokes']
except KeyError: except KeyError: # pragma: no cover
return HttpResponse("No Stroke Data in JSON", status=400) return HttpResponse("No Stroke Data in JSON", status=400)
try: try:
df = pd.DataFrame(strokes['data']) df = pd.DataFrame(strokes['data'])
except KeyError: except KeyError: # pragma: no cover
try: try:
df = pd.DataFrame(request.data['strokedata']) df = pd.DataFrame(request.data['strokedata'])
except: except:
@@ -431,7 +544,7 @@ def strokedatajson_v3(request):
status, comment, data = api_get_dataframe(startdatetime, df) status, comment, data = api_get_dataframe(startdatetime, df)
if status != 200: if status != 200: # pragma: no cover
return HttpResponse(comment, status=status) return HttpResponse(comment, status=status)
@@ -541,7 +654,7 @@ def strokedatajson_v2(request, id):
try: try:
for d in request.data['data']: for d in request.data['data']:
dologging('apilog.log',json.dumps(d)) dologging('apilog.log',json.dumps(d))
except KeyError: except KeyError: # pragma: no cover
try: try:
for d in request.data['strokedata']: for d in request.data['strokedata']:
dologging('apilog.log',json.dumps(d)) dologging('apilog.log',json.dumps(d))
@@ -565,7 +678,7 @@ def strokedatajson_v2(request, id):
df.sort_index(inplace=True) df.sort_index(inplace=True)
status, comment, data = api_get_dataframe(row.startdatetime, df) status, comment, data = api_get_dataframe(row.startdatetime, df)
if status != 200: if status != 200: # pragma: no cover
return HttpResponse(comment, status=status) return HttpResponse(comment, status=status)
r = getrower(request.user) r = getrower(request.user)
+20 -1
View File
@@ -7,7 +7,7 @@ from django.contrib.gis.geoip2 import GeoIP2
from django import forms from django import forms
from rowers.plannedsessions import timefield_to_seconds_duration from rowers.plannedsessions import timefield_to_seconds_duration
from rowers.courses import getnearestraces, getnearestcourses from rowers.courses import getnearestraces, getnearestcourses,coursetokml, coursestokml
# List Courses # List Courses
@@ -16,6 +16,25 @@ def courses_view(request):
r = getrower(request.user) r = getrower(request.user)
g = GeoIP2() g = GeoIP2()
if request.method == 'POST':
try:
tdict = dict(request.POST.lists())
ids = tdict['courseid']
courseids = [int(id) for id in ids]
kmlstring = coursestokml(courseids)
kmlfilename = 'courses.kml'
response = HttpResponse(kmlstring)
response['Content-Disposition'] = 'attachment; filename="{filename}"'.format(
filename=kmlfilename)
response['Content-Type'] = 'application/octet-stream'
return response
except KeyError:
pass
ip = request.META.get('HTTP_X_REAL_IP', '1.1.1.1') ip = request.META.get('HTTP_X_REAL_IP', '1.1.1.1')
try: try:
lat_lon = g.lat_lon(ip) lat_lon = g.lat_lon(ip)
+1 -1
View File
@@ -22,7 +22,7 @@ from scipy.special import lambertw
from io import BytesIO from io import BytesIO
import rowers.plots as plots import rowers.plots as plots
from rowers.permissions import IsOwnerOrNot, IsCompetitorOrNot from rowers.permissions import IsOwnerOrNot, IsCompetitorOrNot
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.decorators import api_view, renderer_classes, permission_classes from rest_framework.decorators import api_view, renderer_classes, permission_classes
from rest_framework.response import Response from rest_framework.response import Response
from rq.job import Job from rq.job import Job
+7 -7
View File
@@ -1664,7 +1664,7 @@ def course_compare_view(request, id=0):
labeldict = { labeldict = {
int(w.id): w.__str__() for w in workouts int(w.id): w.__str__() for w in workouts
} }
except: except: # pragma: no cover
labeldict = {} labeldict = {}
res = interactive_multiple_compare_chart(workoutids, xparam, yparam, res = interactive_multiple_compare_chart(workoutids, xparam, yparam,
@@ -1846,7 +1846,7 @@ def virtualevent_compare_view(request, id=0):
labeldict = { labeldict = {
int(w.id): w.__str__() for w in workouts int(w.id): w.__str__() for w in workouts
} }
except: except: # pragma: no cover
labeldict = {} labeldict = {}
res = interactive_multiple_compare_chart(workoutids, xparam, yparam, res = interactive_multiple_compare_chart(workoutids, xparam, yparam,
@@ -2048,9 +2048,9 @@ def workouts_bulk_actions(request):
w = get_workout_by_opaqueid(request, encid) w = get_workout_by_opaqueid(request, encid)
if w.user == r: if w.user == r:
workouts.append(w) workouts.append(w)
else: else: # pragma: no cover
messages.error(request,'Bulk actions are not accessible to coaches') messages.error(request,'Bulk actions are not accessible to coaches')
except KeyError: except KeyError: # pragma: no cover
pass pass
if request.method == 'POST': if request.method == 'POST':
@@ -2058,7 +2058,7 @@ def workouts_bulk_actions(request):
form = WorkoutMultipleCompareForm(request.POST) form = WorkoutMultipleCompareForm(request.POST)
if form.is_valid() and actionform.is_valid(): if form.is_valid() and actionform.is_valid():
workouts = form.cleaned_data['workouts'] workouts = form.cleaned_data['workouts']
if len(workouts) == 0: if len(workouts) == 0: # pragma: no cover
url = reverse('workouts_view') url = reverse('workouts_view')
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
action = actionform.cleaned_data['action'] action = actionform.cleaned_data['action']
@@ -2078,14 +2078,14 @@ def workouts_bulk_actions(request):
'Workout {id} exported to {destination}'.format( 'Workout {id} exported to {destination}'.format(
id=encoder.encode_hex(w.id), id=encoder.encode_hex(w.id),
destination=destination)) destination=destination))
except NoTokenError: except NoTokenError: # pragma: no cover
messages.error(request, messages.error(request,
'Export to {destination} of workout {id} failed'.format( 'Export to {destination} of workout {id} failed'.format(
id=encoder.encode_hex(w.id), id=encoder.encode_hex(w.id),
destination=destination)) destination=destination))
url = reverse('workouts_view') url = reverse('workouts_view')
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
else: else: # pragma: no cover
if len(workouts) == 0: if len(workouts) == 0:
url = reverse(workouts_view) url = reverse(workouts_view)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)