Skip to content

Commit

Permalink
Merge branch 'refs/heads/feat-elevation-gain'
Browse files Browse the repository at this point in the history
# Conflicts:
#	.github/workflows/run_data_sync.yml
#	README-CN.md
#	README.md
#	run_page/generator/__init__.py
#	run_page/generator/db.py
#	src/components/RunTable/RunRow.tsx
#	src/components/RunTable/style.module.css
#	src/components/YearStat/index.tsx
  • Loading branch information
ben-29 committed Aug 20, 2024
2 parents 9d587ff + 93c36f8 commit 6bb4743
Show file tree
Hide file tree
Showing 19 changed files with 192 additions and 38 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/run_data_sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ on:

env:
# please change to your own config.
RUN_TYPE: pass # support strava/nike/garmin/coros/garmin_cn/garmin_sync_cn_global/keep/only_gpx/only_fit/nike_to_strava/strava_to_garmin/tcx_to_garmin/strava_to_garmin_cn/garmin_to_strava/garmin_to_strava_cn/codoon/oppo, Please change the 'pass' it to your own
RUN_TYPE: pass # support strava/nike/garmin/coros/garmin_cn/garmin_sync_cn_global/keep/only_gpx/only_fit/nike_to_strava/strava_to_garmin/tcx_to_garmin/strava_to_garmin_cn/garmin_to_strava/garmin_to_strava_cn/codoon/oppo/db_updater, Please change the 'pass' it to your own
ATHLETE: ben_29
TITLE: Workouts
MIN_GRID_DISTANCE: 10 # change min distance here
Expand Down Expand Up @@ -202,6 +202,11 @@ jobs:
# If you want to sync fit activity in gpx format, please consider the following script:
# python run_page/oppo_sync.py ${{ secrets.OPPO_ID }} ${{ secrets.OPPO_CLIENT_SECRET }} ${{ secrets.OPPO_CLIENT_REFRESH_TOKEN }} --with-gpx

- name: Run db updater script to add "Elevation Gain" field to db
if: env.RUN_TYPE == 'db_updater'
run: |
python run_page/db_updater.py
- name: Make svg GitHub profile
if: env.RUN_TYPE != 'pass'
run: |
Expand Down
9 changes: 9 additions & 0 deletions README-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## note2: 2023.09.26 garmin need secret_string(and in Actions) get `python run_page/garmin_sync.py ${secret_string}` if cn `python run_page/garmin_sync.py ${secret_string} --is-cn`

## note3: 2024.08.19: Added `Elevation Gain` field, If you forked the project before this update, please run the following command:
- To resolve errors: `sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: activities.elevation_gain`
- For old data: To include `Elevation Gain` for past activities, perform a full reimport.
- If you don't have a local environment, set `RUN_TYPE` to `db_updater` in the `.github/workflows/run_data_sync.yml` file once then change back.

```bash
python run_page/db_updater.py
```

# [打造个人户外运动主页](http://workouts.ben29.xyz)

![screenshot](https://user-images.githubusercontent.com/6956444/163125711-24d0ad99-490d-4c04-b89f-5b7fe776eb38.png)
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## note2: 2023.09.26 garmin need secret_string(and in Actions) get `python run_page/garmin_sync.py ${secret_string}` if cn `python run_page/garmin_sync.py ${secret_string} --is-cn`

## note3: 2024.08.19: Added `Elevation Gain` field, If you forked the project before this update, please run the following command:
- To resolve errors: `sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: activities.elevation_gain`
- For old data: To include `Elevation Gain` for past activities, perform a full reimport.
- If you don't have a local environment, set `RUN_TYPE` to `db_updater` in the `.github/workflows/run_data_sync.yml` file once then change back.

```bash
python run_page/db_updater.py
```

# [Create a personal workouts home page](http://workouts.ben29.xyz)

![screenshot](https://user-images.githubusercontent.com/6956444/163125711-24d0ad99-490d-4c04-b89f-5b7fe776eb38.png)
Expand Down
16 changes: 10 additions & 6 deletions run_page/codoon_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ def parse_points_to_gpx(self, run_points_data):
for p in points_dict_list:
point = gpxpy.gpx.GPXTrackPoint(**p)
gpx_segment.points.append(point)
return gpx.to_xml()
return gpx

def get_single_run_record(self, route_id):
print(f"Get single run for codoon id {route_id}")
Expand Down Expand Up @@ -528,11 +528,14 @@ def parse_raw_data_to_namedtuple(
p["latitude"] = latlng_data[i][0]
p["longitude"] = latlng_data[i][1]

if with_gpx:
# pass the track no points
if str(log_id) not in old_gpx_ids and run_points_data:
gpx_data = self.parse_points_to_gpx(run_points_data)
download_codoon_gpx(gpx_data, str(log_id))
elevation_gain = None
if run_points_data:
gpx_data = self.parse_points_to_gpx(run_points_data)
elevation_gain = gpx_data.get_uphill_downhill().uphill
if with_gpx:
# pass the track no points
if str(log_id) not in old_gpx_ids:
download_codoon_gpx(gpx_data.to_xml(), str(log_id))
heart_rate_dict = run_data.get("heart_rate")
heart_rate = None
if heart_rate_dict:
Expand Down Expand Up @@ -569,6 +572,7 @@ def parse_raw_data_to_namedtuple(
seconds=int((end_date.timestamp() - start_date.timestamp()))
),
"average_speed": run_data["total_length"] / run_data["total_time"],
"elevation_gain": elevation_gain,
"location_country": location_country,
"source": "Codoon",
}
Expand Down
25 changes: 25 additions & 0 deletions run_page/db_updater.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from generator.db import init_db, Activity
from config import SQL_FILE
import sqlalchemy
from sqlalchemy import text
from config import GPX_FOLDER, JSON_FILE, SQL_FILE
from utils import make_activities_file


def add_column_elevation_gain(session):
# check if column elevation_gain is already added
# if not add it to the db
try:
session.query(Activity).first()
print("column elevation_gain already added, skipping")
except sqlalchemy.exc.OperationalError:
sql_statement = 'alter TABLE "activities" add column elevation_gain Float after average_heartrate'
session.execute(text(sql_statement))
print("column elevation_gain added successfully")


if __name__ == "__main__":
session = init_db(SQL_FILE)
add_column_elevation_gain(session)
# regenerate activities
make_activities_file(SQL_FILE, GPX_FOLDER, JSON_FILE)
1 change: 1 addition & 0 deletions run_page/endomondo_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def parse_run_endomondo_to_nametuple(en_dict):
"average_speed": en_dict.get("distance_km", 0)
/ en_dict.get("duration_s", 1)
* 1000,
"elevation_gain": None,
"location_country": "",
}
return namedtuple("x", d.keys())(*d.values())
Expand Down
4 changes: 3 additions & 1 deletion run_page/generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from .db import Activity, init_db, update_or_create_activity

from synced_data_file_logger import save_synced_data_file_list, load_fit_name_mapping
from synced_data_file_logger import save_synced_data_file_list


IGNORE_BEFORE_SAVING = os.getenv("IGNORE_BEFORE_SAVING", False)
Expand Down Expand Up @@ -67,6 +67,8 @@ def sync(self, force):
if IGNORE_BEFORE_SAVING:
activity.summary_polyline = filter_out(activity.summary_polyline)
activity.source = "strava"
# strava use total_elevation_gain as elevation_gain
activity.elevation_gain = activity.total_elevation_gain
created = update_or_create_activity(self.session, activity)
if created:
sys.stdout.write("+")
Expand Down
4 changes: 4 additions & 0 deletions run_page/generator/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def randomword():
"summary_polyline",
"average_heartrate",
"average_speed",
"elevation_gain",
"source",
]

Expand All @@ -55,6 +56,7 @@ class Activity(Base):
summary_polyline = Column(String)
average_heartrate = Column(Float)
average_speed = Column(Float)
elevation_gain = Column(Float)
streak = None
source = Column(String)

Expand Down Expand Up @@ -118,6 +120,7 @@ def update_or_create_activity(session, run_activity):
location_country=location_country,
average_heartrate=run_activity.average_heartrate,
average_speed=float(run_activity.average_speed),
elevation_gain=float(run_activity.elevation_gain),
summary_polyline=(
run_activity.map and run_activity.map.summary_polyline or ""
),
Expand All @@ -133,6 +136,7 @@ def update_or_create_activity(session, run_activity):
activity.type = type
activity.average_heartrate = run_activity.average_heartrate
activity.average_speed = float(run_activity.average_speed)
activity.elevation_gain = float(run_activity.elevation_gain)
activity.summary_polyline = (
run_activity.map and run_activity.map.summary_polyline or ""
)
Expand Down
11 changes: 11 additions & 0 deletions run_page/gpxtrackposter/track.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __init__(self):
self.length = 0
self.special = False
self.average_heartrate = None
self.elevation_gain = None
self.moving_dict = {}
self.run_id = 0
self.start_latlng = []
Expand Down Expand Up @@ -166,6 +167,7 @@ def _load_tcx_data(self, tcx, file_name):
except:
pass
self.polyline_str = polyline.encode(polyline_container)
self.elevation_gain = tcx.ascent
self.moving_dict = {
"distance": self.length,
"moving_time": datetime.timedelta(seconds=moving_time),
Expand Down Expand Up @@ -250,6 +252,7 @@ def _load_gpx_data(self, gpx):
sum(heart_rate_list) / len(heart_rate_list) if heart_rate_list else None
)
self.moving_dict = self._get_moving_data(gpx)
self.elevation_gain = gpx.get_uphill_downhill().uphill

def _load_fit_data(self, fit: dict):
_polylines = []
Expand All @@ -266,6 +269,9 @@ def _load_fit_data(self, fit: dict):
self.average_heartrate = (
message["avg_heart_rate"] if "avg_heart_rate" in message else None
)
self.elevation_gain = (
message["total_ascent"] if "total_ascent" in message else None
)
self.type = message["sport"].lower()

# moving_dict
Expand Down Expand Up @@ -320,6 +326,10 @@ def append(self, other):
)
self.file_names.extend(other.file_names)
self.special = self.special or other.special
self.average_heartrate = self.average_heartrate or other.average_heartrate
self.elevation_gain = (
self.elevation_gain if self.elevation_gain else 0
) + (other.elevation_gain if other.elevation_gain else 0)
except:
print(
f"something wrong append this {self.end_time},in files {str(self.file_names)}"
Expand Down Expand Up @@ -355,6 +365,7 @@ def to_namedtuple(self):
"average_heartrate": (
int(self.average_heartrate) if self.average_heartrate else None
),
"elevation_gain": (int(self.elevation_gain) if self.elevation_gain else 0),
"map": run_map(self.polyline_str),
"start_latlng": self.start_latlng,
"source": self.source,
Expand Down
72 changes: 55 additions & 17 deletions run_page/joyrun_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from datetime import datetime, timedelta
from hashlib import md5
from urllib.parse import quote
from xml.etree import ElementTree

import gpxpy
import polyline
Expand Down Expand Up @@ -189,13 +190,21 @@ def parse_content_to_ponits(content):

@staticmethod
def parse_points_to_gpx(
run_points_data, start_time, end_time, pause_list, interval=5
run_points_data,
start_time,
end_time,
heart_rate_list=None,
altitude_list=None,
pause_list=[],
interval=5,
):
"""
parse run_data content to gpx object
TODO for now kind of same as `keep` maybe refactor later
:param run_points_data: [[latitude, longitude],...]
:param heart_rate_list: [heart_rate, ...]
:param altitude_list: [altitude, ...]
:param pause_list: [[interval_index, pause_seconds],...]
:param interval: time interval between each point, in seconds
"""
Expand All @@ -211,6 +220,8 @@ def parse_points_to_gpx(
"longitude": point[1],
"time": datetime.utcfromtimestamp(current_time),
}
if altitude_list and len(altitude_list) > index:
points_dict["elevation"] = altitude_list[index]
points_dict_list.append(points_dict)

current_time += interval
Expand All @@ -220,13 +231,14 @@ def parse_points_to_gpx(
current_time += int(pause_list[0][1])
pause_list.pop(0)

points_dict_list.append(
{
"latitude": run_points_data[-1][0],
"longitude": run_points_data[-1][1],
"time": datetime.utcfromtimestamp(end_time),
}
)
last = {
"latitude": run_points_data[-1][0],
"longitude": run_points_data[-1][1],
"time": datetime.utcfromtimestamp(end_time),
}
if altitude_list and len(altitude_list) > len(run_points_data):
last["elevation"] = altitude_list[len(run_points_data) - 1]
points_dict_list.append(last)
segment_list.append(points_dict_list)

# gpx part
Expand All @@ -237,14 +249,24 @@ def parse_points_to_gpx(
gpx.tracks.append(gpx_track)

# add segment list to our GPX track:
i = 0
for point_list in segment_list:
gpx_segment = gpxpy.gpx.GPXTrackSegment()
gpx_track.segments.append(gpx_segment)
for p in point_list:
point = gpxpy.gpx.GPXTrackPoint(**p)
if heart_rate_list and len(heart_rate_list) > i:
gpx_extension_hr = ElementTree.fromstring(
f"""<gpxtpx:TrackPointExtension xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1">
<gpxtpx:hr>{heart_rate_list[i]}</gpxtpx:hr>
</gpxtpx:TrackPointExtension>
"""
)
i += 1
point.extensions.append(gpx_extension_hr)
gpx_segment.points.append(point)

return gpx.to_xml()
return gpx

def get_single_run_record(self, fid):
payload = {
Expand All @@ -267,25 +289,40 @@ def parse_raw_data_to_nametuple(self, run_data, old_gpx_ids, with_gpx=False):
end_time = run_data["endtime"]
pause_list = run_data["pause"]
run_points_data = self.parse_content_to_ponits(run_data["content"])
if with_gpx:
# pass the track no points
if run_points_data:
gpx_data = self.parse_points_to_gpx(
run_points_data, start_time, end_time, pause_list
)
download_joyrun_gpx(gpx_data, str(joyrun_id))
altitude_list = run_data["altitude"]

try:
heart_rate_list = (
eval(run_data["heartrate"]) if run_data["heartrate"] else None
)
except:
print(f"Heart Rate: can not eval for {str(heart_rate_list)}")
print(f"Heart Rate: can not eval for {str(run_data['heartrate'''])}")
try:
altitude_list = eval(altitude_list) if altitude_list else None
except:
print(f"Altitude: can not eval for {str(altitude_list)}")
heart_rate = None
if heart_rate_list:
heart_rate = int(sum(heart_rate_list) / len(heart_rate_list))
# fix #66
if heart_rate < 0:
heart_rate = None
elevation_gain = None
# pass the track no points
if run_points_data:
gpx_data = self.parse_points_to_gpx(
run_points_data,
start_time,
end_time,
heart_rate_list,
altitude_list,
pause_list,
)
elevation_gain = gpx_data.get_uphill_downhill().uphill
if with_gpx:
# pass the track no points
if str(joyrun_id) not in old_gpx_ids:
download_joyrun_gpx(gpx_data.to_xml(), str(joyrun_id))

polyline_str = polyline.encode(run_points_data) if run_points_data else ""
start_latlng = start_point(*run_points_data[0]) if run_points_data else None
Expand Down Expand Up @@ -319,6 +356,7 @@ def parse_raw_data_to_nametuple(self, run_data, old_gpx_ids, with_gpx=False):
seconds=int((run_data["endtime"] - run_data["starttime"]))
),
"average_speed": run_data["meter"] / run_data["second"],
"elevation_gain": elevation_gain,
"location_country": location_country,
"source": "Joyrun",
}
Expand Down
Loading

0 comments on commit 6bb4743

Please sign in to comment.