diff --git a/.github/workflows/run_data_sync.yml b/.github/workflows/run_data_sync.yml
index 52919bcb610..ef40eb8da1f 100644
--- a/.github/workflows/run_data_sync.yml
+++ b/.github/workflows/run_data_sync.yml
@@ -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
@@ -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: |
diff --git a/README-CN.md b/README-CN.md
index 76939ef5938..a6429aeff83 100644
--- a/README-CN.md
+++ b/README-CN.md
@@ -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)
diff --git a/README.md b/README.md
index 6b1f96808bb..0f81194911e 100644
--- a/README.md
+++ b/README.md
@@ -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)
diff --git a/run_page/codoon_sync.py b/run_page/codoon_sync.py
index e0b1020c2ed..7aad65f7ff1 100755
--- a/run_page/codoon_sync.py
+++ b/run_page/codoon_sync.py
@@ -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}")
@@ -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:
@@ -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",
}
diff --git a/run_page/db_updater.py b/run_page/db_updater.py
new file mode 100644
index 00000000000..9777be486d2
--- /dev/null
+++ b/run_page/db_updater.py
@@ -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)
diff --git a/run_page/endomondo_sync.py b/run_page/endomondo_sync.py
index 785c01535d3..cd5d89dbb45 100644
--- a/run_page/endomondo_sync.py
+++ b/run_page/endomondo_sync.py
@@ -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())
diff --git a/run_page/generator/__init__.py b/run_page/generator/__init__.py
index de66d2e70fd..4366b45fe05 100644
--- a/run_page/generator/__init__.py
+++ b/run_page/generator/__init__.py
@@ -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)
@@ -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("+")
diff --git a/run_page/generator/db.py b/run_page/generator/db.py
index d93b28d9ae6..a11c92b3be1 100644
--- a/run_page/generator/db.py
+++ b/run_page/generator/db.py
@@ -36,6 +36,7 @@ def randomword():
"summary_polyline",
"average_heartrate",
"average_speed",
+ "elevation_gain",
"source",
]
@@ -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)
@@ -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 ""
),
@@ -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 ""
)
diff --git a/run_page/gpxtrackposter/track.py b/run_page/gpxtrackposter/track.py
index 3df75e100e2..7802f3d0966 100644
--- a/run_page/gpxtrackposter/track.py
+++ b/run_page/gpxtrackposter/track.py
@@ -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 = []
@@ -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),
@@ -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 = []
@@ -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
@@ -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)}"
@@ -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,
diff --git a/run_page/joyrun_sync.py b/run_page/joyrun_sync.py
index 6b8093bab13..64ab10809c3 100644
--- a/run_page/joyrun_sync.py
+++ b/run_page/joyrun_sync.py
@@ -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
@@ -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
"""
@@ -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
@@ -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
@@ -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"""